1
0
Fork 0

fix(config): match command names and docs, cover parts of sync

This commit is contained in:
ssube 2020-08-20 17:08:57 -05:00 committed by BZ Libby
parent 97d2ec5720
commit 505636f3dc
10 changed files with 337 additions and 82 deletions

View File

@ -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.

View File

@ -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

View File

@ -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> {

View File

@ -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;

View File

@ -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));
}

33
test/TestGraph.ts Normal file
View File

@ -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');
});
});

View File

@ -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);
});
});
});

View File

@ -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');
});

View File

@ -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',
});
});
});

View File

@ -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');
});
});