fix(config): match command names and docs, cover parts of sync
This commit is contained in:
parent
97d2ec5720
commit
505636f3dc
37
docs/arch.md
37
docs/arch.md
|
@ -10,7 +10,8 @@ This guide describes the program architecture.
|
|||
- [Label](#label)
|
||||
- [Flag Label](#flag-label)
|
||||
- [State Label](#state-label)
|
||||
- [State Label Value](#state-label-value)
|
||||
- [State Value](#state-value)
|
||||
- [State Changes](#state-changes)
|
||||
- [Remote](#remote)
|
||||
- [Github Remote](#github-remote)
|
||||
- [Gitlab Remote](#gitlab-remote)
|
||||
|
@ -36,10 +37,42 @@ they are not connected directly.
|
|||
State labels are set from a group. Only one value from each state may be set at a time,
|
||||
and the highest priority value automatically replaces any others.
|
||||
|
||||
#### State Label Value
|
||||
#### State Value
|
||||
|
||||
Individual values within the state.
|
||||
|
||||
#### State Changes
|
||||
|
||||
State labels may replace one value with another using normal change rules, with the addition
|
||||
of a list of matching labels that must be present for the state change to be resolved.
|
||||
|
||||
For example, a state with three values like so:
|
||||
|
||||
```yaml
|
||||
projects:
|
||||
- name: foo
|
||||
flags:
|
||||
- name: next
|
||||
states:
|
||||
- name: status
|
||||
values:
|
||||
- name: new
|
||||
becomes:
|
||||
- adds: [status/in-progress]
|
||||
matches: [next]
|
||||
- name: in-progress
|
||||
becomes:
|
||||
- adds: [status/done]
|
||||
matches: [next]
|
||||
- name: done
|
||||
becomes:
|
||||
- adds: [status/new]
|
||||
matches: [next]
|
||||
```
|
||||
|
||||
Each time `cautious-journey` evaluates the labels on this project, any issues with the `status/new` **and** `next`
|
||||
labels will be promoted to `status/in-progress`, and both the `status/new` and `next` labels will be removed.
|
||||
|
||||
## Remote
|
||||
|
||||
`cautious-journey` manipulates issues and labels that exist on some remote service.
|
||||
|
|
|
@ -20,8 +20,9 @@ This guide explains how to start using `cautious-journey` to manage project and
|
|||
- [Available Remotes](#available-remotes)
|
||||
- [Copying Project Remotes](#copying-project-remotes)
|
||||
- [Commands](#commands)
|
||||
- [Sync Issue Labels](#sync-issue-labels)
|
||||
- [Sync Project Labels](#sync-project-labels)
|
||||
- [Graph Labels](#graph-labels)
|
||||
- [Sync Issues](#sync-issues)
|
||||
- [Sync Projects](#sync-projects)
|
||||
- [Changes](#changes)
|
||||
- [Flag Changes](#flag-changes)
|
||||
- [State Changes](#state-changes)
|
||||
|
@ -145,15 +146,20 @@ TODO: how would you share a remote config block between two projects?
|
|||
|
||||
The program can run in a few different modes:
|
||||
|
||||
- sync issue labels
|
||||
- sync project labels
|
||||
- [`dot-graph`](#graph-labels)
|
||||
- [`sync-issues`](#sync-issue-labels)
|
||||
- [`sync-labels`](#sync-project-labels)
|
||||
|
||||
### Sync Issue Labels
|
||||
### Graph Labels
|
||||
|
||||
This mode will print a `dot` graph, to be formatted with the GraphViz tools.
|
||||
|
||||
### Sync Issues
|
||||
|
||||
This mode will look through each issue on the project, open or closed, and check for label updates. Labels will
|
||||
be resolved using the project's rules, and a comment left recording any changes.
|
||||
|
||||
### Sync Project Labels
|
||||
### Sync Projects
|
||||
|
||||
This mode will look through the project labels, ensuring they are up to date with the project labels in the
|
||||
config. Missing labels will be created, extra labels will be deleted, and existing labels will be updated to
|
||||
|
|
|
@ -4,9 +4,9 @@ import { VERSION_INFO } from '../version';
|
|||
|
||||
export enum Commands {
|
||||
UNKNOWN = 'unknown',
|
||||
GRAPH = 'dot-graph',
|
||||
GRAPH = 'graph-labels',
|
||||
ISSUES = 'sync-issues',
|
||||
LABELS = 'sync-labels',
|
||||
LABELS = 'sync-projects',
|
||||
}
|
||||
|
||||
interface Parser<TData> {
|
||||
|
|
|
@ -2,11 +2,11 @@ import { doesExist, mustExist } from '@apextoaster/js-utils';
|
|||
import { Logger } from 'noicejs';
|
||||
import { prng } from 'seedrandom';
|
||||
|
||||
import { FlagLabel, getLabelColor, getLabelNames, getValueName, StateLabel } from './labels';
|
||||
import { ProjectConfig } from './config';
|
||||
import { getLabelColor, getLabelNames, getValueName } from './labels';
|
||||
import { LabelUpdate, Remote } from './remote';
|
||||
import { resolveLabels } from './resolve';
|
||||
import { defaultTo, defaultUntil, compareItems } from './utils';
|
||||
import { ProjectConfig } from './config';
|
||||
import { compareItems, defaultTo, defaultUntil } from './utils';
|
||||
|
||||
export interface SyncOptions {
|
||||
logger: Logger;
|
||||
|
|
|
@ -35,11 +35,3 @@ export function compareItems<T>(a: Array<T>, b: Array<T>): boolean {
|
|||
|
||||
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));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
describe('graph tools', () => {
|
||||
describe('label edges', () => {
|
||||
it('should include rule labels');
|
||||
});
|
||||
|
||||
describe('graph labels', () => {
|
||||
it('should create nodes for each label');
|
||||
it('should create edges for each change rule');
|
||||
it('should create edges for each state change');
|
||||
});
|
||||
|
||||
describe('clean name', () => {
|
||||
it('should remove special characters');
|
||||
it('should remove duplicate escape characters');
|
||||
});
|
||||
|
||||
describe('edge style', () => {
|
||||
it('should color edges');
|
||||
});
|
||||
|
||||
describe('node style', () => {
|
||||
it('should label nodes');
|
||||
it('should color nodes');
|
||||
});
|
||||
|
||||
describe('dot formatter', () => {
|
||||
it('should print nodes');
|
||||
it('should print edges');
|
||||
it('should cluster flags');
|
||||
it('should cluster states');
|
||||
it('should label the root digraph');
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { expect } from 'chai';
|
||||
|
||||
import { defaultTo, defaultUntil } from '../src/utils';
|
||||
import { defaultTo, defaultUntil, compareItems } from '../src/utils';
|
||||
|
||||
const TEST_TRUE = 'foo';
|
||||
const TEST_FALSE = 'bar';
|
||||
|
@ -25,4 +25,43 @@ describe('utils', () => {
|
|||
expect(defaultUntil(undefined, undefined, TEST_TRUE, undefined)).to.equal(TEST_TRUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compare items helper', () => {
|
||||
/* eslint-disable no-magic-numbers */
|
||||
it('should compare items by reference', () => {
|
||||
const dat = {};
|
||||
expect(compareItems([
|
||||
1, dat, 3,
|
||||
], [
|
||||
1, dat, 3,
|
||||
])).to.equal(true);
|
||||
|
||||
expect(compareItems([
|
||||
1, dat, 3,
|
||||
], [
|
||||
1, {}, 3,
|
||||
])).to.equal(false);
|
||||
|
||||
expect(compareItems([
|
||||
1, 2, 3,
|
||||
], [
|
||||
1, 2, 4,
|
||||
])).to.equal(false);
|
||||
});
|
||||
|
||||
it('should sort arrays before comparison', () => {
|
||||
expect(compareItems([
|
||||
1, 2, 3,
|
||||
], [
|
||||
3, 2, 1,
|
||||
])).to.equal(true);
|
||||
});
|
||||
|
||||
it('should always reject arrays of different lengths', () => {
|
||||
expect(compareItems(
|
||||
new Array(5).fill(1),
|
||||
new Array(3).fill(1)
|
||||
)).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,61 @@
|
|||
import { expect } from 'chai';
|
||||
import { NullLogger } from 'noicejs';
|
||||
import { alea } from 'seedrandom';
|
||||
import { stub } from 'sinon';
|
||||
|
||||
import { GithubRemote } from '../../src/remote/github';
|
||||
import { syncIssueLabels } from '../../src/sync';
|
||||
|
||||
describe('issue sync', () => {
|
||||
it('should resolve each issue');
|
||||
it('should resolve each issue', async () => {
|
||||
const logger = NullLogger.global;
|
||||
const remoteData = {
|
||||
data: {},
|
||||
dryrun: true,
|
||||
logger,
|
||||
type: '',
|
||||
};
|
||||
const remote = new GithubRemote(remoteData);
|
||||
const listStub = stub(remote, 'listIssues').returns(Promise.resolve([{
|
||||
issue: '',
|
||||
labels: ['nope'],
|
||||
name: '',
|
||||
project: 'foo',
|
||||
}]));
|
||||
const updateStub = stub(remote, 'updateIssue').returns(Promise.resolve({
|
||||
issue: '',
|
||||
labels: [],
|
||||
name: '',
|
||||
project: '',
|
||||
}));
|
||||
|
||||
await syncIssueLabels({
|
||||
logger,
|
||||
project: {
|
||||
colors: [],
|
||||
comment: false,
|
||||
flags: [{
|
||||
adds: [],
|
||||
name: 'nope',
|
||||
priority: 0,
|
||||
removes: [],
|
||||
requires: [{
|
||||
name: 'yep',
|
||||
}],
|
||||
}],
|
||||
name: 'foo',
|
||||
remote: remoteData,
|
||||
states: [],
|
||||
},
|
||||
random: alea(),
|
||||
remote,
|
||||
});
|
||||
expect(listStub).to.have.callCount(1);
|
||||
expect(updateStub).to.have.callCount(1);
|
||||
expect(updateStub).to.have.been.calledWithMatch({
|
||||
project: 'foo',
|
||||
});
|
||||
});
|
||||
|
||||
it('should update issues with label changes');
|
||||
});
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import { expect } from 'chai';
|
||||
import { alea } from 'seedrandom';
|
||||
import { match, spy } from 'sinon';
|
||||
|
||||
import { BunyanLogger } from '../../src/logger/bunyan';
|
||||
import { GithubRemote } from '../../src/remote/github';
|
||||
import { syncSingleLabel } from '../../src/sync';
|
||||
|
||||
describe('label sync', () => {
|
||||
it('should sync each label');
|
||||
it('should pick a stable random color for each label', async () => {
|
||||
const logger = BunyanLogger.create({
|
||||
name: 'test',
|
||||
});
|
||||
const remoteConfig = {
|
||||
data: {},
|
||||
dryrun: true,
|
||||
logger,
|
||||
type: '',
|
||||
};
|
||||
const remote = new GithubRemote(remoteConfig);
|
||||
const updateSpy = spy(remote, 'updateLabel');
|
||||
|
||||
await syncSingleLabel({
|
||||
logger,
|
||||
project: {
|
||||
colors: [
|
||||
'ff0000',
|
||||
],
|
||||
comment: true,
|
||||
flags: [{
|
||||
adds: [],
|
||||
name: 'foo',
|
||||
priority: 1,
|
||||
removes: [],
|
||||
requires: [],
|
||||
}],
|
||||
name: '',
|
||||
remote: remoteConfig,
|
||||
states: [],
|
||||
},
|
||||
random: alea(),
|
||||
remote,
|
||||
}, {
|
||||
color: '',
|
||||
desc: '',
|
||||
name: 'foo',
|
||||
project: '',
|
||||
});
|
||||
|
||||
expect(updateSpy).to.have.callCount(1);
|
||||
|
||||
const COLOR_LENGTH = 6;
|
||||
expect(updateSpy).to.have.been.calledWithMatch({
|
||||
color: match.string.and(match((it) => it.length === COLOR_LENGTH)),
|
||||
name: 'foo',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,156 @@
|
|||
import { expect } from 'chai';
|
||||
import { alea } from 'seedrandom';
|
||||
import { match, spy, stub } from 'sinon';
|
||||
|
||||
import { BunyanLogger } from '../../src/logger/bunyan';
|
||||
import { GithubRemote } from '../../src/remote/github';
|
||||
import { syncProjectLabels, syncSingleLabel } from '../../src/sync';
|
||||
|
||||
describe('project sync', () => {
|
||||
describe('all labels', () => {
|
||||
it('should sync each label');
|
||||
it('should pick a stable random color for each label', async () => {
|
||||
const logger = BunyanLogger.create({
|
||||
name: 'test',
|
||||
});
|
||||
const remoteConfig = {
|
||||
data: {},
|
||||
dryrun: true,
|
||||
logger,
|
||||
type: '',
|
||||
};
|
||||
const remote = new GithubRemote(remoteConfig);
|
||||
const updateSpy = spy(remote, 'updateLabel');
|
||||
|
||||
await syncSingleLabel({
|
||||
logger,
|
||||
project: {
|
||||
colors: [
|
||||
'ff0000',
|
||||
],
|
||||
comment: true,
|
||||
flags: [{
|
||||
adds: [],
|
||||
name: 'foo',
|
||||
priority: 1,
|
||||
removes: [],
|
||||
requires: [],
|
||||
}],
|
||||
name: '',
|
||||
remote: remoteConfig,
|
||||
states: [],
|
||||
},
|
||||
random: alea(),
|
||||
remote,
|
||||
}, {
|
||||
color: '',
|
||||
desc: '',
|
||||
name: 'foo',
|
||||
project: '',
|
||||
});
|
||||
|
||||
expect(updateSpy).to.have.callCount(1);
|
||||
|
||||
const COLOR_LENGTH = 6;
|
||||
expect(updateSpy).to.have.been.calledWithMatch({
|
||||
color: match.string.and(match((it) => it.length === COLOR_LENGTH)),
|
||||
name: 'foo',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create missing labels', async () => {
|
||||
const logger = BunyanLogger.create({
|
||||
name: 'test',
|
||||
});
|
||||
const remoteConfig = {
|
||||
data: {},
|
||||
dryrun: true,
|
||||
logger,
|
||||
type: '',
|
||||
};
|
||||
const remote = new GithubRemote(remoteConfig);
|
||||
const createStub = stub(remote, 'createLabel');
|
||||
const deleteStub = stub(remote, 'deleteLabel');
|
||||
const listStub = stub(remote, 'listLabels').returns(Promise.resolve([]));
|
||||
|
||||
await syncProjectLabels({
|
||||
logger,
|
||||
project: {
|
||||
colors: [],
|
||||
comment: true,
|
||||
flags: [{
|
||||
adds: [],
|
||||
color: '',
|
||||
desc: '',
|
||||
name: '',
|
||||
priority: 1,
|
||||
removes: [],
|
||||
requires: [],
|
||||
}],
|
||||
name: '',
|
||||
remote: remoteConfig,
|
||||
states: [],
|
||||
},
|
||||
random: alea(),
|
||||
remote,
|
||||
});
|
||||
|
||||
expect(listStub).to.have.callCount(1);
|
||||
expect(createStub).to.have.callCount(1);
|
||||
expect(deleteStub).to.have.callCount(0);
|
||||
});
|
||||
|
||||
it('should delete extra labels', async () => {
|
||||
const logger = BunyanLogger.create({
|
||||
name: 'test',
|
||||
});
|
||||
const remoteConfig = {
|
||||
data: {},
|
||||
dryrun: true,
|
||||
logger,
|
||||
type: '',
|
||||
};
|
||||
const remote = new GithubRemote(remoteConfig);
|
||||
const createStub = stub(remote, 'createLabel');
|
||||
const deleteStub = stub(remote, 'deleteLabel');
|
||||
const listStub = stub(remote, 'listLabels').returns(Promise.resolve([{
|
||||
color: '',
|
||||
desc: '',
|
||||
name: '',
|
||||
project: '',
|
||||
}]));
|
||||
|
||||
await syncProjectLabels({
|
||||
logger,
|
||||
project: {
|
||||
colors: [],
|
||||
comment: true,
|
||||
flags: [],
|
||||
name: '',
|
||||
remote: remoteConfig,
|
||||
states: [],
|
||||
},
|
||||
random: alea(),
|
||||
remote,
|
||||
});
|
||||
|
||||
expect(listStub).to.have.callCount(1);
|
||||
expect(createStub).to.have.callCount(0);
|
||||
expect(deleteStub).to.have.callCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flag labels', () => {
|
||||
it('should prefer flag color');
|
||||
});
|
||||
|
||||
describe('state labels', () => {
|
||||
it('should prefer value color');
|
||||
it('should fall back to state color');
|
||||
it('should use state divider');
|
||||
});
|
||||
|
||||
describe('create label', () => {
|
||||
it('should create label');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue