feat(config): add project comment, state divider to config
This commit is contained in:
parent
3a3facb282
commit
2feb5a3c60
|
@ -1,11 +1,11 @@
|
||||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||||
|
|
||||||
[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
|
||||||
|
|
||||||
<b>Signature:</b>
|
<b>Signature:</b>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
flags: Array<FlagLabel>;
|
divider: string;
|
||||||
```
|
```
|
|
@ -17,5 +17,6 @@ export interface StateLabel extends BaseLabel
|
||||||
|
|
||||||
| Property | Type | Description |
|
| 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. |
|
| [values](./cautious-journey.statelabel.values.md) | Array<[StateValue](./cautious-journey.statevalue.md)<!-- -->> | Values for this state. |
|
||||||
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
|
||||||
|
|
||||||
[Home](./index.md) > [cautious-journey](./cautious-journey.md) > [SyncOptions](./cautious-journey.syncoptions.md) > [colors](./cautious-journey.syncoptions.colors.md)
|
|
||||||
|
|
||||||
## SyncOptions.colors property
|
|
||||||
|
|
||||||
<b>Signature:</b>
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
colors: Array<string>;
|
|
||||||
```
|
|
|
@ -14,11 +14,8 @@ export interface SyncOptions
|
||||||
|
|
||||||
| Property | Type | Description |
|
| 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 | |
|
| [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 | |
|
| [random](./cautious-journey.syncoptions.random.md) | prng | |
|
||||||
| [remote](./cautious-journey.syncoptions.remote.md) | [Remote](./cautious-journey.remote.md) | |
|
| [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. |
|
|
||||||
|
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
<b>Signature:</b>
|
<b>Signature:</b>
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
project: string;
|
project: ProjectConfig;
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
|
||||||
|
|
||||||
[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.
|
|
||||||
|
|
||||||
<b>Signature:</b>
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
states: Array<StateLabel>;
|
|
||||||
```
|
|
|
@ -5,40 +5,51 @@ import { FlagLabel, StateLabel } from '../labels';
|
||||||
import { RemoteOptions } from '../remote';
|
import { RemoteOptions } from '../remote';
|
||||||
import * as SCHEMA_DATA from './schema.yml';
|
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<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leave a comment along with any update, explaining the changes that were made.
|
||||||
|
*
|
||||||
|
* @default `true`
|
||||||
|
*/
|
||||||
|
comment: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Individual flag labels.
|
||||||
|
*/
|
||||||
|
flags: Array<FlagLabel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project name or path.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote APIs.
|
||||||
|
*/
|
||||||
|
remote: RemoteOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grouped state labels.
|
||||||
|
*/
|
||||||
|
states: Array<StateLabel>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config data for the app, loaded from CLI or DOM.
|
* Config data for the app, loaded from CLI or DOM.
|
||||||
*/
|
*/
|
||||||
export interface ConfigData {
|
export interface ConfigData {
|
||||||
logger: {
|
logger: LoggerConfig;
|
||||||
level: LogLevel;
|
projects: Array<ProjectConfig>;
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
projects: Array<{
|
|
||||||
/**
|
|
||||||
* Color palette for labels without their own.
|
|
||||||
*/
|
|
||||||
colors: Array<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Individual flag labels.
|
|
||||||
*/
|
|
||||||
flags: Array<FlagLabel>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project name or path.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote APIs.
|
|
||||||
*/
|
|
||||||
remote: RemoteOptions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grouped state labels.
|
|
||||||
*/
|
|
||||||
states: Array<StateLabel>;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,6 +65,7 @@ export function initConfig(): ConfigData {
|
||||||
},
|
},
|
||||||
projects: [{
|
projects: [{
|
||||||
colors: [],
|
colors: [],
|
||||||
|
comment: true,
|
||||||
flags: [],
|
flags: [],
|
||||||
name: '',
|
name: '',
|
||||||
remote: {
|
remote: {
|
||||||
|
@ -82,5 +94,11 @@ export function validateConfig(it: unknown): it is ConfigData {
|
||||||
const ajv = new Ajv(SCHEMA_OPTIONS);
|
const ajv = new Ajv(SCHEMA_OPTIONS);
|
||||||
ajv.addSchema(SCHEMA_DATA, 'cautious-journey');
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,17 @@ $id: cautious-journey
|
||||||
definitions:
|
definitions:
|
||||||
label-ref:
|
label-ref:
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
change-set:
|
change-set:
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- adds
|
||||||
|
- removes
|
||||||
properties:
|
properties:
|
||||||
adds:
|
adds:
|
||||||
type: array
|
type: array
|
||||||
|
@ -23,6 +28,9 @@ definitions:
|
||||||
|
|
||||||
base-label:
|
base-label:
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- requires
|
||||||
properties:
|
properties:
|
||||||
color:
|
color:
|
||||||
type: string
|
type: string
|
||||||
|
@ -59,7 +67,13 @@ definitions:
|
||||||
- $ref: "#/definitions/change-set"
|
- $ref: "#/definitions/change-set"
|
||||||
- $ref: "#/definitions/base-label"
|
- $ref: "#/definitions/base-label"
|
||||||
- type: object
|
- type: object
|
||||||
|
required:
|
||||||
|
- divider
|
||||||
|
- values
|
||||||
properties:
|
properties:
|
||||||
|
divider:
|
||||||
|
type: string
|
||||||
|
default: "/"
|
||||||
values:
|
values:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
@ -71,6 +85,8 @@ definitions:
|
||||||
- $ref: "#/definitions/change-set"
|
- $ref: "#/definitions/change-set"
|
||||||
- $ref: "#/definitions/base-label"
|
- $ref: "#/definitions/base-label"
|
||||||
- type: object
|
- type: object
|
||||||
|
required:
|
||||||
|
- becomes
|
||||||
properties:
|
properties:
|
||||||
becomes:
|
becomes:
|
||||||
type: array
|
type: array
|
||||||
|
@ -78,38 +94,60 @@ definitions:
|
||||||
$ref: "#/definitions/state-change"
|
$ref: "#/definitions/state-change"
|
||||||
default: []
|
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:
|
config:
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- logger
|
||||||
|
- projects
|
||||||
properties:
|
properties:
|
||||||
logger:
|
logger:
|
||||||
type: object
|
$ref: "#/definitions/logger"
|
||||||
properties:
|
|
||||||
level:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
projects:
|
projects:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: object
|
$ref: "#/definitions/project"
|
||||||
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: []
|
|
||||||
|
|
||||||
type: object
|
type: object
|
|
@ -14,7 +14,7 @@ const STATUS_ERROR = 1;
|
||||||
*/
|
*/
|
||||||
main(process.argv).then((status) => process.exit(status)).catch((err: Error) => {
|
main(process.argv).then((status) => process.exit(status)).catch((err: Error) => {
|
||||||
// eslint-disable-next-line no-console
|
// 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);
|
process.exit(STATUS_ERROR);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,8 @@ export interface StateValue extends BaseLabel {
|
||||||
* Grouped labels: the equivalent of a radio group.
|
* Grouped labels: the equivalent of a radio group.
|
||||||
*/
|
*/
|
||||||
export interface StateLabel extends BaseLabel {
|
export interface StateLabel extends BaseLabel {
|
||||||
|
divider: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Values for this state.
|
* Values for this state.
|
||||||
*/
|
*/
|
||||||
|
@ -97,12 +99,12 @@ export function getLabelNames(flags: Array<FlagLabel>, states: Array<StateLabel>
|
||||||
return new Set(labels);
|
return new Set(labels);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function splitValueName(name: string): Array<string> {
|
export function splitValueName(state: StateLabel, name: string): Array<string> {
|
||||||
return name.split('/');
|
return name.split(state.divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getValueName(state: StateLabel, value: StateValue): string {
|
export function getValueName(state: StateLabel, value: StateValue): string {
|
||||||
return `${state.name}/${value.name}`;
|
return `${state.name}${state.divider}${value.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
21
src/main.ts
21
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 { createSchema } from '@apextoaster/js-yaml-schema';
|
||||||
import { existsSync, readFileSync, realpathSync } from 'fs';
|
import { existsSync, readFileSync, realpathSync } from 'fs';
|
||||||
import { DEFAULT_SAFE_SCHEMA, safeLoad } from 'js-yaml';
|
import { DEFAULT_SAFE_SCHEMA, safeLoad } from 'js-yaml';
|
||||||
|
@ -7,11 +7,11 @@ import { alea } from 'seedrandom';
|
||||||
|
|
||||||
import { ConfigData, validateConfig } from './config';
|
import { ConfigData, validateConfig } from './config';
|
||||||
import { Commands, createParser } from './config/args';
|
import { Commands, createParser } from './config/args';
|
||||||
|
import { dotGraph, graphLabels } from './graph';
|
||||||
import { BunyanLogger } from './logger/bunyan';
|
import { BunyanLogger } from './logger/bunyan';
|
||||||
import { GithubRemote } from './remote/github';
|
import { GithubRemote } from './remote/github';
|
||||||
import { syncIssueLabels, SyncOptions, syncProjectLabels } from './sync';
|
import { syncIssueLabels, SyncOptions, syncProjectLabels } from './sync';
|
||||||
import { VERSION_INFO } from './version';
|
import { VERSION_INFO } from './version';
|
||||||
import { graphLabels, dotGraph } from './graph';
|
|
||||||
|
|
||||||
export { FlagLabel, StateLabel } from './labels';
|
export { FlagLabel, StateLabel } from './labels';
|
||||||
export { Remote, RemoteOptions } from './remote';
|
export { Remote, RemoteOptions } from './remote';
|
||||||
|
@ -38,7 +38,7 @@ async function loadConfig(path: string): Promise<ConfigData> {
|
||||||
const config = safeLoad(rawConfig, { schema });
|
const config = safeLoad(rawConfig, { schema });
|
||||||
|
|
||||||
if (!validateConfig(config)) {
|
if (!validateConfig(config)) {
|
||||||
throw new Error();
|
throw new InvalidArgumentError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return config as ConfigData;
|
return config as ConfigData;
|
||||||
|
@ -52,14 +52,16 @@ export async function main(argv: Array<string>): Promise<number> {
|
||||||
const logger = BunyanLogger.create(config.logger);
|
const logger = BunyanLogger.create(config.logger);
|
||||||
|
|
||||||
logger.info({
|
logger.info({
|
||||||
args,
|
|
||||||
config,
|
|
||||||
mode,
|
mode,
|
||||||
version: VERSION_INFO,
|
version: VERSION_INFO,
|
||||||
}, 'startup environment');
|
}, 'running main');
|
||||||
|
logger.debug({
|
||||||
|
args,
|
||||||
|
config,
|
||||||
|
}, 'runtime data');
|
||||||
|
|
||||||
for (const project of config.projects) {
|
for (const project of config.projects) {
|
||||||
const { colors, flags, name, states } = project;
|
const { name } = project;
|
||||||
|
|
||||||
if (doesExist(args.project) && !args.project.includes(name)) {
|
if (doesExist(args.project) && !args.project.includes(name)) {
|
||||||
logger.info({ project: name }, 'skipping project');
|
logger.info({ project: name }, 'skipping project');
|
||||||
|
@ -77,13 +79,10 @@ export async function main(argv: Array<string>): Promise<number> {
|
||||||
|
|
||||||
// mode switch
|
// mode switch
|
||||||
const options: SyncOptions = {
|
const options: SyncOptions = {
|
||||||
colors,
|
|
||||||
flags,
|
|
||||||
logger,
|
logger,
|
||||||
project: name,
|
project,
|
||||||
random,
|
random,
|
||||||
remote,
|
remote,
|
||||||
states,
|
|
||||||
};
|
};
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case Commands.GRAPH:
|
case Commands.GRAPH:
|
||||||
|
|
|
@ -133,40 +133,45 @@ export function resolveLabels(options: ResolveInput): ResolveResult {
|
||||||
label: name,
|
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;
|
const combinedValue: BaseLabel = {
|
||||||
for (const become of value.becomes) {
|
adds: [...state.adds, ...value.adds],
|
||||||
if (become.matches.every((l) => activeLabels.has(l.name))) {
|
name,
|
||||||
checkLabelRules({
|
priority: defaultUntil(value.priority, state.priority, 0),
|
||||||
...combinedValue,
|
removes: [...state.removes, ...value.removes],
|
||||||
adds: become.adds,
|
requires: [...state.requires, ...value.requires],
|
||||||
removes: [...become.matches, ...become.removes],
|
};
|
||||||
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,
|
||||||
});
|
});
|
||||||
|
removed = true;
|
||||||
if (activeLabels.delete(name)) {
|
|
||||||
changes.push({
|
|
||||||
cause: name,
|
|
||||||
effect: ChangeVerb.REMOVED,
|
|
||||||
label: name,
|
|
||||||
});
|
|
||||||
removed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removed) {
|
if (removed) {
|
||||||
|
|
105
src/sync.ts
105
src/sync.ts
|
@ -6,25 +6,13 @@ import { FlagLabel, getLabelColor, getLabelNames, getValueName, StateLabel } fro
|
||||||
import { LabelUpdate, Remote } from './remote';
|
import { LabelUpdate, Remote } from './remote';
|
||||||
import { resolveLabels } from './resolve';
|
import { resolveLabels } from './resolve';
|
||||||
import { defaultTo, defaultUntil, compareItems } from './utils';
|
import { defaultTo, defaultUntil, compareItems } from './utils';
|
||||||
|
import { ProjectConfig } from './config';
|
||||||
|
|
||||||
export interface SyncOptions {
|
export interface SyncOptions {
|
||||||
/**
|
|
||||||
*/
|
|
||||||
colors: Array<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
flags: Array<FlagLabel>;
|
|
||||||
|
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
project: string;
|
project: ProjectConfig;
|
||||||
random: prng;
|
random: prng;
|
||||||
remote: Remote;
|
remote: Remote;
|
||||||
|
|
||||||
/**
|
|
||||||
* States from project config.
|
|
||||||
*/
|
|
||||||
states: Array<StateLabel>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,34 +20,38 @@ export interface SyncOptions {
|
||||||
* if there are changes and no errors, then updates the issue.
|
* if there are changes and no errors, then updates the issue.
|
||||||
*/
|
*/
|
||||||
export async function syncIssueLabels(options: SyncOptions): Promise<unknown> {
|
export async function syncIssueLabels(options: SyncOptions): Promise<unknown> {
|
||||||
const issues = await options.remote.listIssues({
|
const { logger, project, remote } = options;
|
||||||
project: options.project,
|
const issues = await remote.listIssues({
|
||||||
|
project: project.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const issue of issues) {
|
for (const issue of issues) {
|
||||||
options.logger.info({ issue }, 'project issue');
|
logger.info({ issue }, 'project issue');
|
||||||
|
|
||||||
const { changes, errors, labels } = resolveLabels({
|
const { changes, errors, labels } = resolveLabels({
|
||||||
flags: options.flags,
|
flags: project.flags,
|
||||||
labels: issue.labels,
|
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
|
// TODO: prompt user to update this particular issue
|
||||||
const sameLabels = compareItems(issue.labels, labels) || changes.length === 0;
|
const sameLabels = compareItems(issue.labels, labels) || changes.length === 0;
|
||||||
if (sameLabels === false && errors.length === 0) {
|
if (sameLabels === false && errors.length === 0) {
|
||||||
options.logger.info({ changes, errors, issue, labels }, 'updating issue');
|
logger.info({ changes, errors, issue, labels }, 'updating issue');
|
||||||
await options.remote.updateIssue({
|
await remote.updateIssue({
|
||||||
...issue,
|
...issue,
|
||||||
labels,
|
labels,
|
||||||
});
|
});
|
||||||
await options.remote.createComment({
|
|
||||||
...issue,
|
if (project.comment) {
|
||||||
changes,
|
await remote.createComment({
|
||||||
errors,
|
...issue,
|
||||||
});
|
changes,
|
||||||
|
errors,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,19 +59,21 @@ export async function syncIssueLabels(options: SyncOptions): Promise<unknown> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncProjectLabels(options: SyncOptions): Promise<unknown> {
|
export async function syncProjectLabels(options: SyncOptions): Promise<unknown> {
|
||||||
const labels = await options.remote.listLabels({
|
const { logger, project, remote } = options;
|
||||||
project: options.project,
|
|
||||||
|
const labels = await remote.listLabels({
|
||||||
|
project: project.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
const present = new Set(labels.map((l) => l.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]);
|
const combined = new Set([...desired, ...present]);
|
||||||
|
|
||||||
for (const label of combined) {
|
for (const label of combined) {
|
||||||
const exists = present.has(label);
|
const exists = present.has(label);
|
||||||
const expected = desired.has(label);
|
const expected = desired.has(label);
|
||||||
|
|
||||||
options.logger.info({
|
logger.info({
|
||||||
exists,
|
exists,
|
||||||
expected,
|
expected,
|
||||||
label,
|
label,
|
||||||
|
@ -90,15 +84,15 @@ export async function syncProjectLabels(options: SyncOptions): Promise<unknown>
|
||||||
const data = mustExist(labels.find((l) => l.name === label));
|
const data = mustExist(labels.find((l) => l.name === label));
|
||||||
await syncSingleLabel(options, data);
|
await syncSingleLabel(options, data);
|
||||||
} else {
|
} else {
|
||||||
options.logger.warn({ label }, 'remove label');
|
logger.warn({ label }, 'remove label');
|
||||||
await options.remote.deleteLabel({
|
await remote.deleteLabel({
|
||||||
name: label,
|
name: label,
|
||||||
project: options.project,
|
project: project.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (expected) {
|
if (expected) {
|
||||||
options.logger.info({ label }, 'create label');
|
logger.info({ label }, 'create label');
|
||||||
await createLabel(options, label);
|
await createLabel(options, label);
|
||||||
} else {
|
} else {
|
||||||
// skip
|
// skip
|
||||||
|
@ -110,27 +104,29 @@ export async function syncProjectLabels(options: SyncOptions): Promise<unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createLabel(options: SyncOptions, name: string) {
|
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)) {
|
if (doesExist(flag)) {
|
||||||
await options.remote.createLabel({
|
await remote.createLabel({
|
||||||
color: getLabelColor(options.colors, options.random, flag),
|
color: getLabelColor(project.colors, options.random, flag),
|
||||||
desc: mustExist(flag.desc),
|
desc: mustExist(flag.desc),
|
||||||
name,
|
name,
|
||||||
project: options.project,
|
project: project.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = options.states.find((it) => name.startsWith(it.name));
|
const state = project.states.find((it) => name.startsWith(it.name));
|
||||||
if (doesExist(state)) {
|
if (doesExist(state)) {
|
||||||
const value = state.values.find((it) => getValueName(state, it) === name);
|
const value = state.values.find((it) => getValueName(state, it) === name);
|
||||||
if (doesExist(value)) {
|
if (doesExist(value)) {
|
||||||
await options.remote.createLabel({
|
await remote.createLabel({
|
||||||
color: getLabelColor(options.colors, options.random, state, value),
|
color: getLabelColor(project.colors, options.random, state, value),
|
||||||
desc: defaultUntil(value.desc, state.desc, ''),
|
desc: defaultUntil(value.desc, state.desc, ''),
|
||||||
name: getValueName(state, value),
|
name: getValueName(state, value),
|
||||||
project: options.project,
|
project: project.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -139,6 +135,8 @@ export async function createLabel(options: SyncOptions, name: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncLabelDiff(options: SyncOptions, oldLabel: LabelUpdate, newLabel: LabelUpdate) {
|
export async function syncLabelDiff(options: SyncOptions, oldLabel: LabelUpdate, newLabel: LabelUpdate) {
|
||||||
|
const { logger, project } = options;
|
||||||
|
|
||||||
const dirty =
|
const dirty =
|
||||||
oldLabel.color !== mustExist(newLabel.color) ||
|
oldLabel.color !== mustExist(newLabel.color) ||
|
||||||
oldLabel.desc !== mustExist(newLabel.desc);
|
oldLabel.desc !== mustExist(newLabel.desc);
|
||||||
|
@ -148,41 +146,40 @@ export async function syncLabelDiff(options: SyncOptions, oldLabel: LabelUpdate,
|
||||||
color: defaultTo(newLabel.color, oldLabel.color),
|
color: defaultTo(newLabel.color, oldLabel.color),
|
||||||
desc: defaultTo(newLabel.desc, oldLabel.desc),
|
desc: defaultTo(newLabel.desc, oldLabel.desc),
|
||||||
name: oldLabel.name,
|
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);
|
const resp = await options.remote.updateLabel(body);
|
||||||
|
logger.debug({ resp }, 'update response');
|
||||||
options.logger.debug({ resp }, 'update resp');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncSingleLabel(options: SyncOptions, label: LabelUpdate): Promise<void> {
|
export async function syncSingleLabel(options: SyncOptions, label: LabelUpdate): Promise<void> {
|
||||||
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)) {
|
if (doesExist(flag)) {
|
||||||
const color = getLabelColor(options.colors, options.random, flag);
|
const color = getLabelColor(project.colors, options.random, flag);
|
||||||
await syncLabelDiff(options, label, {
|
await syncLabelDiff(options, label, {
|
||||||
color,
|
color,
|
||||||
desc: defaultTo(flag.desc, label.desc),
|
desc: defaultTo(flag.desc, label.desc),
|
||||||
name: flag.name,
|
name: flag.name,
|
||||||
project: options.project,
|
project: project.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
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)) {
|
if (doesExist(state)) {
|
||||||
const value = state.values.find((it) => getValueName(state, it) === label.name);
|
const value = state.values.find((it) => getValueName(state, it) === label.name);
|
||||||
if (doesExist(value)) {
|
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, {
|
await syncLabelDiff(options, label, {
|
||||||
color,
|
color,
|
||||||
desc: defaultTo(value.desc, label.desc),
|
desc: defaultTo(value.desc, label.desc),
|
||||||
name: getValueName(state, value),
|
name: getValueName(state, value),
|
||||||
project: options.project,
|
project: project.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -35,3 +35,11 @@ export function compareItems<T>(a: Array<T>, b: Array<T>): boolean {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Collection<T> {
|
||||||
|
has(value: T): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contains<T>(a: Collection<T>, b: Array<T>): boolean {
|
||||||
|
return b.every((it) => a.has(it));
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { alea } from 'seedrandom';
|
import { alea } from 'seedrandom';
|
||||||
|
|
||||||
import { getLabelColor, getLabelNames, prioritySort } from '../src/labels';
|
import { getLabelColor, getLabelNames, prioritySort, StateLabel } from '../src/labels';
|
||||||
|
|
||||||
describe('label helpers', () => {
|
describe('label helpers', () => {
|
||||||
describe('label name helper', () => {
|
describe('label name helper', () => {
|
||||||
|
@ -38,8 +38,9 @@ describe('label helpers', () => {
|
||||||
removes: [],
|
removes: [],
|
||||||
requires: [],
|
requires: [],
|
||||||
}];
|
}];
|
||||||
const states = [{
|
const states: Array<StateLabel> = [{
|
||||||
adds: [],
|
adds: [],
|
||||||
|
divider: '/',
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
removes: [],
|
removes: [],
|
||||||
|
@ -47,6 +48,7 @@ describe('label helpers', () => {
|
||||||
values,
|
values,
|
||||||
}, {
|
}, {
|
||||||
adds: [],
|
adds: [],
|
||||||
|
divider: '/',
|
||||||
name: 'bar',
|
name: 'bar',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
removes: [],
|
removes: [],
|
||||||
|
@ -109,6 +111,7 @@ describe('label helpers', () => {
|
||||||
expect(getLabelColor(['test'], alea(), {
|
expect(getLabelColor(['test'], alea(), {
|
||||||
adds: [],
|
adds: [],
|
||||||
color: 'beans',
|
color: 'beans',
|
||||||
|
divider: '/',
|
||||||
name: '',
|
name: '',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
removes: [],
|
removes: [],
|
||||||
|
@ -129,6 +132,7 @@ describe('label helpers', () => {
|
||||||
expect(getLabelColor(['test'], alea(), {
|
expect(getLabelColor(['test'], alea(), {
|
||||||
adds: [],
|
adds: [],
|
||||||
color: 'beans',
|
color: 'beans',
|
||||||
|
divider: '/',
|
||||||
name: '',
|
name: '',
|
||||||
priority: 1,
|
priority: 1,
|
||||||
removes: [],
|
removes: [],
|
||||||
|
|
|
@ -12,30 +12,35 @@ describe('label sync', () => {
|
||||||
const logger = BunyanLogger.create({
|
const logger = BunyanLogger.create({
|
||||||
name: 'test',
|
name: 'test',
|
||||||
});
|
});
|
||||||
const remote = new GithubRemote({
|
const remoteConfig = {
|
||||||
data: {},
|
data: {},
|
||||||
dryrun: true,
|
dryrun: true,
|
||||||
logger,
|
logger,
|
||||||
type: '',
|
type: '',
|
||||||
});
|
};
|
||||||
|
const remote = new GithubRemote(remoteConfig);
|
||||||
const updateSpy = spy(remote, 'updateLabel');
|
const updateSpy = spy(remote, 'updateLabel');
|
||||||
|
|
||||||
await syncSingleLabel({
|
await syncSingleLabel({
|
||||||
colors: [
|
|
||||||
'ff0000',
|
|
||||||
],
|
|
||||||
flags: [{
|
|
||||||
adds: [],
|
|
||||||
name: 'foo',
|
|
||||||
priority: 1,
|
|
||||||
removes: [],
|
|
||||||
requires: [],
|
|
||||||
}],
|
|
||||||
logger,
|
logger,
|
||||||
project: '',
|
project: {
|
||||||
|
colors: [
|
||||||
|
'ff0000',
|
||||||
|
],
|
||||||
|
comment: true,
|
||||||
|
flags: [{
|
||||||
|
adds: [],
|
||||||
|
name: 'foo',
|
||||||
|
priority: 1,
|
||||||
|
removes: [],
|
||||||
|
requires: [],
|
||||||
|
}],
|
||||||
|
name: '',
|
||||||
|
remote: remoteConfig,
|
||||||
|
states: [],
|
||||||
|
},
|
||||||
random: alea(),
|
random: alea(),
|
||||||
remote,
|
remote,
|
||||||
states: [],
|
|
||||||
}, {
|
}, {
|
||||||
color: '',
|
color: '',
|
||||||
desc: '',
|
desc: '',
|
||||||
|
|
Loading…
Reference in New Issue