Skip to content

Commit 6b531aa

Browse files
authored
add schema validation and checklist rendering
1 parent b16c7c6 commit 6b531aa

22 files changed

+305
-94
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"jest": "^24.9.0",
4444
"jest-circus": "^26.4.2",
4545
"js-yaml": "^3.14.0",
46+
"jsonschema": "^1.3.0",
4647
"prettier": "2.1.1",
4748
"ts-jest": "^24.3.0",
4849
"typescript": "^4.0.2",

schemas/IConfig.json

+28-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,21 @@
3333
"footerFileUrl": {
3434
"description": "File that will be added as the issue footer.\nExample: \"https://raw.githubusercontent.com/legomushroom/codespaces-board/main/sprints/sprint%2012/footer.md\"",
3535
"type": "string"
36+
},
37+
"isReplaceProjectMarkers": {
38+
"description": "If replace the <!-- codespaces-board:project_{id}:start -->\nmarkers instead of replacing entire issue body.\n\ndefault: false.",
39+
"type": "boolean"
40+
},
41+
"$schema": {
42+
"description": "Used by `vscode` in JSON files.",
43+
"type": "string"
3644
}
3745
},
46+
"additionalProperties": false,
47+
"required": [
48+
"boardIssue",
49+
"repos"
50+
],
3851
"definitions": {
3952
"IRepoSourceConfig": {
4053
"type": "object",
@@ -61,7 +74,12 @@
6174
]
6275
}
6376
}
64-
}
77+
},
78+
"additionalProperties": false,
79+
"required": [
80+
"owner",
81+
"repo"
82+
]
6583
},
6684
"IProject": {
6785
"type": "object",
@@ -76,8 +94,16 @@
7694
"items": {
7795
"type": "string"
7896
}
97+
},
98+
"isCheckListItems": {
99+
"description": "If to render issues as check list using the [x] markers.\n\ndefault: false",
100+
"type": "boolean"
79101
}
80-
}
102+
},
103+
"additionalProperties": false,
104+
"required": [
105+
"id"
106+
]
81107
}
82108
},
83109
"$schema": "http://json-schema.org/draft-07/schema#"

scripts/generate-json-schema.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ if (!fs.existsSync(OUTPUT_SCHEMA_FOLDER_PATH)){
1212
}
1313

1414
// schema generator settings
15-
const settings = {};
15+
const settings = {
16+
required: true,
17+
strictNullChecks: true,
18+
noExtraProps: true,
19+
};
1620

1721
// ts compiler options
1822
const compilerOptions = {

src/config.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
1+
import { Schema, Validator } from 'jsonschema';
2+
import * as path from 'path';
3+
4+
import { PROJECT_ROOT } from './constants';
5+
16
import { IConfig } from './interfaces/IConfig';
27

38
export const getConfigs = (configFilePath: string): IConfig[] => {
4-
const configs = require(configFilePath);
9+
const configs = require(path.join(PROJECT_ROOT, configFilePath));
10+
11+
return configs;
12+
};
13+
14+
export const getConfigSchema = (): Schema => {
15+
const schema = require(path.join(PROJECT_ROOT, `./schemas/IConfig.json`));
16+
17+
return schema;
18+
};
19+
20+
export const validateConfig = (config: unknown) => {
21+
const validator = new Validator();
22+
23+
const validationResult = validator.validate(config, getConfigSchema());
24+
const { errors } = validationResult;
525

6-
return configs;
26+
return errors;
727
};

src/constants.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export const PROJECT_ROOT = '../';
1+
import * as path from 'path';
2+
3+
export const PROJECT_ROOT = path.join(__dirname, '../');

src/interfaces/IConfig.ts

+13
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,17 @@ export interface IConfig {
4747
* Example: "https://raw.githubusercontent.com/legomushroom/codespaces-board/main/sprints/sprint%2012/footer.md"
4848
*/
4949
footerFileUrl?: string;
50+
51+
/**
52+
* If replace the <!-- codespaces-board:project_{id}:start -->
53+
* markers instead of replacing entire issue body.
54+
*
55+
* default: false.
56+
*/
57+
isReplaceProjectMarkers?: boolean;
58+
59+
/**
60+
* Used by `vscode` in JSON files.
61+
*/
62+
['$schema']?: string,
5063
}

src/interfaces/IIssueState.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum IIssueState {
2+
Open = 'open',
3+
Closed = 'closed',
4+
}

src/interfaces/IProject.ts

+8
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ export interface IProject {
55
* @TJS-type integer
66
*/
77
id: number;
8+
89
/**
910
* Issue labels that will be rendered as sections
1011
* on the aggregated issue.
1112
*/
1213
trackLabels?: string[];
14+
15+
/**
16+
* If to render issues as check list using the [x] markers.
17+
*
18+
* default: false
19+
*/
20+
isCheckListItems?: boolean;
1321
}

src/interfaces/IProjectWithConfig.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { TProject } from './TProject';
2+
import { TProjectConfig } from './TProjetConfig';
3+
4+
5+
export interface IProjectWithConfig {
6+
project: TProject;
7+
projectConfig: TProjectConfig;
8+
}

src/interfaces/IProjectWithTrackedLabels.ts

-7
This file was deleted.

src/interfaces/IRepoSourceConfig.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IProject } from './IProject';
1+
import { TProjectConfig } from './TProjetConfig';
22

33
export interface IRepoSourceConfig {
44
/**
@@ -14,5 +14,5 @@ export interface IRepoSourceConfig {
1414
/**
1515
* Project to track, if not set assuming all projects on the repo.
1616
*/
17-
projects?: (IProject | number)[];
17+
projects?: TProjectConfig[];
1818
}

src/interfaces/TProjetConfig.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { IProject } from './IProject';
2+
3+
export type TProjectConfig = IProject | number;

src/main.ts

+69-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as core from '@actions/core';
2-
import { env } from './utils/env';
2+
import { Validator } from 'jsonschema';
33

44
// add .env file support for dev purposes
55
require('dotenv').config();
@@ -9,14 +9,79 @@ import { ProjectsOctoKit } from './octokit/ProjectsOctoKit';
99
import { renderProject } from './views/renderProject';
1010
import { renderOverview } from './views/renderOverview';
1111
import { getProjectData } from './utils/getProjectData';
12-
import { getConfigs } from './config';
12+
import { env } from './utils/env';
13+
import { getConfigs, validateConfig } from './config';
14+
1315
import { IConfig } from './interfaces/IConfig';
1416

1517
const TOKEN_NAME = 'REPO_GITHUB_PAT';
1618
const CONFIG_PATH = 'CONFIG_PATH';
1719

20+
21+
const overwriteBoardIssue = async (issueContents: string, config: IConfig, projectKit: ProjectsOctoKit) => {
22+
const { status } = await projectKit.updateBoardIssue(
23+
config.boardIssue,
24+
issueContents,
25+
);
26+
27+
if (status !== 200) {
28+
throw new Error(
29+
`Failed to update the issue ${config.boardIssue}`,
30+
);
31+
}
32+
33+
console.log(`Successfully updated the board issue ${config.boardIssue}`);
34+
}
35+
36+
const getRegex = (projectId?: number) => {
37+
const regex = /<!--\s*codespaces-board:start\s*-->([\W\w]*)<!--\s*codespaces-board:end\s*-->/gim;
38+
return regex;
39+
}
40+
41+
const wrapIssueText = (text: string, projectId?: number) => {
42+
return [
43+
`<!-- codespaces-board:start -->`,
44+
`<!-- ⚠️ AUTO GENERATED GITHUB ACTION, DON'T EDIT BY HAND ⚠️ -->`,
45+
`<!-- updated on: ${new Date().toISOString()} -->`,
46+
text,
47+
`<!-- codespaces-board:end -->`,
48+
].join('\n');
49+
}
50+
51+
const updateBoardIssue = async (issueContents: string, config: IConfig, projectKit: ProjectsOctoKit) => {
52+
if (!config.isReplaceProjectMarkers) {
53+
return await overwriteBoardIssue(
54+
issueContents,
55+
config,
56+
projectKit,
57+
);
58+
}
59+
60+
const issue = await projectKit.getBoardIssue(
61+
config.boardIssue,
62+
issueContents,
63+
);
64+
65+
const { body } = issue;
66+
const newBody = body.replace(getRegex(), wrapIssueText(issueContents));
67+
68+
await overwriteBoardIssue(
69+
newBody,
70+
config,
71+
projectKit,
72+
);
73+
}
74+
1875
const processConfigRecord = async (config: IConfig, projectKit: ProjectsOctoKit) => {
19-
console.log(`Processing config for issue ${config.boardIssue}`);
76+
console.log('Processing config: \n', config);
77+
78+
const validationErrors = validateConfig(config);
79+
if (validationErrors.length) {
80+
console.error(`\n\nNot valid config for the issue ${config.boardIssue}, skipping.. \n`, validationErrors, '\n\n');
81+
return;
82+
}
83+
84+
console.log(`Config schema validation passed.`);
2085

2186
const repoProjects = await projectKit.getAllProjects(config.repos);
2287

@@ -47,29 +112,14 @@ const processConfigRecord = async (config: IConfig, projectKit: ProjectsOctoKit)
47112
footer = await projectKit.getBoardHeaderText(config.footerFileUrl);
48113
}
49114

50-
const projectsData = projectsWithData.map((x) => {
51-
return x.data;
52-
});
53-
54115
const issueContents = [
55116
header,
56117
renderOverview(config, projectsWithData),
57118
issueBody,
58119
footer
59120
].join('\n');
60121

61-
const { status } = await projectKit.updateBoardIssue(
62-
config.boardIssue,
63-
issueContents,
64-
);
65-
66-
if (status !== 200) {
67-
throw new Error(
68-
`Failed to update the issue ${config.boardIssue}`,
69-
);
70-
}
71-
72-
console.log(`Successfully updated the board issue ${config.boardIssue}`);
122+
await updateBoardIssue(issueContents, config, projectKit);
73123
}
74124
}
75125

src/octokit/ProjectsOctoKit.ts

+26-10
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { TRepoIssue } from '../interfaces/TRepoIssue';
1212
import { TColumnTypes } from '../interfaces/TColumnTypes';
1313
import { IWrappedIssue } from '../interfaces/IWrappedIssue';
1414
import { IProject } from '../interfaces/IProject';
15-
import { IProjectWithTrackedLabels } from '../interfaces/IProjectWithTrackedLabels';
15+
import { IProjectWithConfig } from '../interfaces/IProjectWithConfig';
1616

1717
type TColumnsMap = Record<TColumnTypes, TProjectColumn | undefined>;
1818

@@ -56,7 +56,7 @@ const getProjectId = (project: IProject | number) => {
5656
export class ProjectsOctoKit extends OctoKitBase {
5757
public getRepoProjects = async (
5858
repo: IRepoSourceConfig,
59-
): Promise<IProjectWithTrackedLabels[]> => {
59+
): Promise<IProjectWithConfig[]> => {
6060
const { data: projectsResponse } = await this.kit.projects.listForRepo({
6161
accept: 'application/vnd.github.inertia-preview+json',
6262
owner: repo.owner,
@@ -65,7 +65,7 @@ export class ProjectsOctoKit extends OctoKitBase {
6565
});
6666

6767
const fetchedProjects = projectsResponse.map((project):
68-
| IProjectWithTrackedLabels
68+
| IProjectWithConfig
6969
| undefined => {
7070
const { projects } = repo;
7171

@@ -81,13 +81,9 @@ export class ProjectsOctoKit extends OctoKitBase {
8181
return;
8282
}
8383

84-
const labels = (typeof proj === 'number')
85-
? []
86-
: proj.trackLabels ?? [];
87-
8884
return {
8985
project,
90-
labels,
86+
projectConfig: proj,
9187
};
9288
})
9389
.filter(notEmpty);
@@ -97,7 +93,7 @@ export class ProjectsOctoKit extends OctoKitBase {
9793

9894
public getAllProjects = async (
9995
repos: IRepoSourceConfig[],
100-
): Promise<{ repo: IRepoSourceConfig; projects: IProjectWithTrackedLabels[] }[]> => {
96+
): Promise<{ repo: IRepoSourceConfig; projects: IProjectWithConfig[] }[]> => {
10197
const result = [];
10298

10399
for (let repo of repos) {
@@ -111,7 +107,7 @@ export class ProjectsOctoKit extends OctoKitBase {
111107
return result;
112108
};
113109

114-
public getColumns = async (projectWithLabels: IProjectWithTrackedLabels): Promise<TColumnsMap> => {
110+
public getColumns = async (projectWithLabels: IProjectWithConfig): Promise<TColumnsMap> => {
115111
const { project } = projectWithLabels;
116112

117113
const { data: columns } = await this.kit.projects.listColumns({
@@ -218,6 +214,26 @@ export class ProjectsOctoKit extends OctoKitBase {
218214
});
219215
};
220216

217+
public getBoardIssue = async (issueUrl: string, body: string) => {
218+
const { owner, repo, issueNumber } = parseIssueUrl(issueUrl);
219+
220+
const { status, data } = await this.kit.issues.get({
221+
owner,
222+
repo,
223+
issue_number: issueNumber,
224+
body,
225+
});
226+
227+
if (status !== 200) {
228+
throw new Error(
229+
`Failed to get the issue ${issueUrl}`,
230+
);
231+
}
232+
233+
return data;
234+
};
235+
236+
221237
public getBoardHeaderText = async (fileUrl: string): Promise<string> => {
222238
const fileRef = parseFileUrl(fileUrl);
223239

0 commit comments

Comments
 (0)