├── .github
├── ISSUE_TEMPLATE
│ ├── enhancement.yml
│ ├── ex-prep.yml
│ ├── ex-story-standalone.yml
│ ├── ex-story.yml
│ └── story.yml
├── actions
│ ├── forward-project-field.mjs
│ ├── octonode.d.ts
│ ├── score-triaged-defects.mjs
│ └── sync-labels.ts
├── labels.yml
└── workflows
│ ├── score-triaged-defects.yml
│ ├── static_analysis.yml
│ ├── sync-labels.yml
│ ├── triage-move-labelled.yml
│ ├── triage-move-unlabelled.yml
│ └── x-plorers-epic-forwarding.yml
├── .gitignore
├── README.md
├── docs
├── FTUE.md
├── auth
│ └── enhanced_idm_integration.md
├── chat_effects.md
├── client_well_known.md
├── crypto
│ ├── backup.md
│ ├── trust_v1.md
│ └── trust_v2.md
├── device_management.md
├── keyboard_shortcuts.md
├── notifications_defaultsettings.md
├── profile_signout.md
├── roomlist_messagePreviews.md
├── roomlist_sort.md
├── rooms_invitejoinleaveban.md
└── text_effects.md
├── package.json
├── spec
├── functional_members.md
└── matrix_client_information.md
├── tsconfig.json
├── wiki-images
├── test-cases.png
├── test-cases.svg
├── testing-issue-list.png
├── testing-issues.png
├── testing-issues.svg
├── testing-tabs.png
└── testing-test-cases.png
└── yarn.lock
/.github/ISSUE_TEMPLATE/enhancement.yml:
--------------------------------------------------------------------------------
1 | name: Enhancement request
2 | description: Do you have a suggestion or feature request?
3 | labels: [T-Enhancement]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion](https://github.com/vector-im/element-meta/discussions/new?category=ideas).
9 | - type: textarea
10 | id: usecase
11 | attributes:
12 | label: Your use case
13 | description: Please feel welcome to include screenshots or mock ups.
14 | placeholder: Tell us what you would like to do!
15 | value: |
16 | #### What would you like to do?
17 |
18 | #### Why would you like to do it?
19 |
20 | #### How would you like to achieve it?
21 | validations:
22 | required: true
23 | - type: textarea
24 | id: alternative
25 | attributes:
26 | label: Have you considered any alternatives?
27 | placeholder: A clear and concise description of any alternative solutions or features you've considered.
28 | validations:
29 | required: false
30 | - type: textarea
31 | id: additional-context
32 | attributes:
33 | label: Additional context
34 | placeholder: Is there anything else you'd like to add?
35 | validations:
36 | required: false
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ex-prep.yml:
--------------------------------------------------------------------------------
1 | name: EX – Prep task
2 | description: A preparatory task without direct user value
3 | title: "[Prep]
"
4 | labels: ["App: ElementX Android", "App: ElementX iOS", "T-Prep", "Team: Element X Platform"]
5 |
6 | body:
7 | - type: textarea
8 | attributes:
9 | label: Description
10 | value: |
11 | Description of the task
12 |
13 | # Acceptance criteria
14 | - TBD
15 |
16 | # Size estimate
17 | S/M/L
18 |
19 | # Dependencies
20 | - None
21 |
22 | # Out of scope
23 | -
24 |
25 | # Subtasks
26 | ```[tasklist]
27 | ### Android
28 | ```
29 |
30 | ```[tasklist]
31 | ### iOS
32 | ```
33 |
34 | ```[tasklist]
35 | ### Rust
36 | ```
37 |
38 | ```[tasklist]
39 | ### Other
40 | ```
41 | validations:
42 | required: false
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ex-story-standalone.yml:
--------------------------------------------------------------------------------
1 | name: EX – Standalone User story
2 | description: Template for a standalone user story with no parent projet or epic. This a small "epic" with the appropriate process and labels that fit in the team's planning board.
3 | title: "[Story] "
4 | labels: ["App: ElementX Android", "App: ElementX iOS", "T-User Story", "T-Epic", "Team: Element X Platform"]
5 |
6 | body:
7 | - type: textarea
8 | attributes:
9 | label: Description
10 | value: |
11 | * As a [beneficiary]
12 | * I want to [objective]
13 | * So that [value generated]
14 |
15 | ### Acceptance criteria
16 | - TBD
17 |
18 | ### Leads
19 | * Tech:
20 | * Design:
21 |
22 | ### Time sheeting
23 |
24 | ?
25 |
26 | ### Documentation
27 | -
28 |
29 | ### Dependencies
30 | - None
31 |
32 | ### Out of scope
33 | - Nothing
34 |
35 | ### Open questions
36 | - [ ]
37 |
38 | ## Subtasks
39 |
40 | ### Android
41 | -
42 |
43 | ### iOS
44 | -
45 |
46 | ### Rust
47 | -
48 |
49 | ### Other
50 | -
51 |
52 | ## Sign-offs
53 | ### Android
54 | - [ ] Design sign-off on completion
55 | - [ ] QA sign-off on completion
56 | - [ ] Product sign-off on completion
57 |
58 | ### iOS
59 | - [ ] Design sign-off on completion
60 | - [ ] QA sign-off on completion
61 | - [ ] Product sign-off on completion
62 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ex-story.yml:
--------------------------------------------------------------------------------
1 | name: EX – User story
2 | description: Template for a second-level planning issue template. It must be part of an Epic.
3 | title: "[Story] "
4 | labels: ["App: ElementX Android", "App: ElementX iOS", "T-User Story", "Team: Element X Platform"]
5 |
6 | body:
7 | - type: textarea
8 | attributes:
9 | label: Description
10 | value: |
11 | * As a [beneficiary]
12 | * I want to [objective]
13 | * So that [value generated]
14 |
15 | ### Acceptance criteria
16 | - TBD
17 |
18 | ### Dependencies
19 | - None
20 |
21 | ### Out of scope
22 | - Nothing
23 |
24 | ### Questions
25 | - [ ]
26 |
27 | ## Subtasks
28 |
29 | ### Android
30 | -
31 |
32 | ### iOS
33 | -
34 |
35 | ### Rust
36 | -
37 |
38 | ### Other
39 | -
40 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/story.yml:
--------------------------------------------------------------------------------
1 | name: User story
2 | description: Second-level planning issue template. A story should take about a week or a sprint to finish. It is normally one of a number of parts of an Epic
3 | title: "[Story] "
4 | labels: [T-User Story]
5 |
6 | body:
7 | - type: textarea
8 | attributes:
9 | label: Story
10 | description: A story should take roughly a week or a sprint to finish. Each story is usually made up of a number of tasks that take half to a full day.
11 | value: |
12 | As a user…
13 | I want to…
14 | so that I can…
15 | - type: textarea
16 | attributes:
17 | label: Dependencies
18 | value: |
19 | - TBD
20 | - type: textarea
21 | attributes:
22 | label: Sign-offs
23 | value: |
24 | #### Android
25 | - [ ] Design sign-off on completion
26 | - [ ] QA sign-off on completion
27 | - [ ] Product sign-off on completion
28 |
29 | #### iOS
30 | - [ ] Design sign-off on completion
31 | - [ ] QA sign-off on completion
32 | - [ ] Product sign-off on completion
33 |
34 | #### Web
35 | - [ ] Design sign-off on completion
36 | - [ ] QA sign-off on completion
37 | - [ ] Product sign-off on completion
38 | - type: textarea
39 | attributes:
40 | label: Scope
41 | value: |
42 |
43 | ```[tasklist]
44 | ### Android
45 | - [ ] Verified with build in screen reader
46 | ```
47 |
48 | ```[tasklist]
49 | ### iOS
50 | - [ ] Verified with build in screen reader
51 | ```
52 |
53 | ```[tasklist]
54 | ### Web
55 | - [ ] Verified with system in screen reader
56 | ```
57 |
58 | ## Out of scope
59 |
60 | - TBD
61 |
62 | validations:
63 | required: false
64 |
--------------------------------------------------------------------------------
/.github/actions/forward-project-field.mjs:
--------------------------------------------------------------------------------
1 | import { Octokit } from "@octokit/action";
2 |
3 | const octokit = new Octokit();
4 |
5 | const headers = { "GraphQL-Features": "projects_next_graphql" }
6 |
7 | const REPO_OWNER = process.env.REPO_OWNER;
8 | const REPO_NAME = process.env.REPO_NAME;
9 | const ISSUE_URL = process.env.ISSUE_URL;
10 | const ISSUE_NUMBER = parseInt(process.env.ISSUE_NUMBER);
11 | const PROJECT_ID = process.env.PROJECT_ID;
12 | const FIELD_ID = process.env.FIELD_ID;
13 | const FIELD_NAME = process.env.FIELD_NAME;
14 |
15 | const visitedIssueUrls = new Set();
16 |
17 | async function queryFieldValue(repoOwner, repoName, issueNumber, fieldName) {
18 | const query = `query ($owner: String!, $repo: String!, $issueNumber: Int!, $fieldName: String!) {
19 | repository(owner: $owner, name: $repo) {
20 | issue(number: $issueNumber) {
21 | projectItems(first: 100) {
22 | edges {
23 | node {
24 | id
25 | fieldValueByName(name: $fieldName) {
26 | ... on ProjectV2ItemFieldSingleSelectValue {
27 | optionId
28 | name
29 | field {
30 | ... on ProjectV2SingleSelectField {
31 | id
32 | }
33 | }
34 | }
35 | }
36 | project {
37 | id
38 | }
39 | }
40 | }
41 | }
42 | }
43 | }
44 | }`;
45 |
46 | const parameters = {
47 | owner: repoOwner,
48 | repo: repoName,
49 | issueNumber: issueNumber,
50 | fieldName: fieldName,
51 | headers
52 | };
53 |
54 | const result = await octokit.graphql(query, parameters);
55 |
56 | return result.repository.issue.projectItems.edges;
57 | }
58 |
59 | function determineFieldValue(projectItems, projectId, fieldId) {
60 | for (const item of projectItems) {
61 | if (item.node.project.id == projectId && item.node.fieldValueByName && item.node.fieldValueByName.field.id == fieldId) {
62 | return { name: item.node.fieldValueByName.name, id: item.node.fieldValueByName.optionId };
63 | }
64 | }
65 |
66 | return {}
67 | }
68 |
69 | async function setFieldValueOnTrackedIssues(repoOwner, repoName, issueUrl, issueNumber, projectId, fieldId, fieldName, fieldValue) {
70 | // Avoid infinite loop
71 |
72 | if (visitedIssueUrls.has(issueUrl)) {
73 | return;
74 | }
75 | visitedIssueUrls.add(issueUrl);
76 |
77 | // Make sure this is actually an issue
78 |
79 | if (issueUrl.indexOf("/issues/") < 0) {
80 | return;
81 | }
82 |
83 | // Get tracked issues
84 |
85 | console.log(`Querying tracked issues of ${issueUrl}`);
86 | const trackedIssues = await queryTrackedIssues(repoOwner, repoName, issueNumber);
87 |
88 | if (!trackedIssues || trackedIssues.length == 0) {
89 | console.log("Aborting because issue has no tracked issues");
90 | return;
91 | }
92 |
93 | console.log(trackedIssues.map(issue => issue.node.url));
94 |
95 | // Set field values
96 |
97 | for (const issue of trackedIssues) {
98 | const trackedIssueId = issue.node.id;
99 | const trackedIssueUrl = issue.node.url;
100 | const trackedIssueNumber = issue.node.number;
101 | const trackedRepoOwner = issue.node.repository.owner.login;
102 | const trackedRepoName = issue.node.repository.name;
103 |
104 | // Get project item or add one if needed
105 |
106 | let itemId = issue.node.projectItems.edges.find(item => item.node.project.id == projectId)?.node?.id;
107 |
108 | if (!itemId) {
109 | console.log(`Adding ${trackedIssueUrl} to project`);
110 | itemId = await addItemToProject(projectId, trackedIssueId);
111 | }
112 |
113 | // Set field value
114 |
115 | console.log(`Setting value "${fieldValue.name}" for field "${fieldName}" of ${trackedIssueUrl}`);
116 | mutateFieldValue(projectId, itemId, fieldId, fieldValue.id);
117 |
118 | // Recurse
119 |
120 | await setFieldValueOnTrackedIssues(trackedRepoOwner, trackedRepoName, trackedIssueUrl, trackedIssueNumber, projectId, fieldId, fieldName, fieldValue);
121 | }
122 | }
123 |
124 | async function queryTrackedIssues(repoOwner, repoName, issueNumber) {
125 | const query = `query ($owner: String!, $repo: String!, $issueNumber: Int!) {
126 | repository(owner: $owner, name: $repo) {
127 | issue(number: $issueNumber) {
128 | trackedIssues(first: 100) {
129 | edges {
130 | node {
131 | projectItems(first: 100) {
132 | edges {
133 | node {
134 | id
135 | project {
136 | id
137 | }
138 | }
139 | }
140 | }
141 | id
142 | url
143 | number
144 | repository {
145 | name
146 | owner {
147 | login
148 | }
149 | }
150 | }
151 | }
152 | }
153 | }
154 | }
155 | }`;
156 |
157 | const parameters = {
158 | owner: repoOwner,
159 | repo: repoName,
160 | issueNumber: issueNumber,
161 | headers
162 | };
163 |
164 | const result = await octokit.graphql(query, parameters);
165 |
166 | return result.repository.issue.trackedIssues.edges;
167 | }
168 |
169 | async function addItemToProject(projectId, contentId) {
170 | const mutation = `mutation ($projectId: ID!, $contentId: ID!) {
171 | addProjectV2ItemById(input: {projectId: $projectId contentId: $contentId}) {
172 | item {
173 | id
174 | }
175 | }
176 | }`;
177 |
178 | const parameters = {
179 | projectId: projectId,
180 | contentId: contentId,
181 | headers
182 | };
183 |
184 | const result = await octokit.graphql(mutation, parameters);
185 |
186 | return result.addProjectV2ItemById.item.id;
187 | }
188 |
189 | async function mutateFieldValue(projectId, itemId, fieldId, fieldValueId) {
190 | const mutation = `mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
191 | updateProjectV2ItemFieldValue(
192 | input: {
193 | projectId: $projectId
194 | itemId: $itemId
195 | fieldId: $fieldId
196 | value: {
197 | singleSelectOptionId: $optionId
198 | }
199 | }
200 | ) {
201 | projectV2Item {
202 | id
203 | }
204 | }
205 | }`;
206 |
207 | const parameters = {
208 | projectId: projectId,
209 | itemId: itemId,
210 | fieldId: fieldId,
211 | optionId: fieldValueId,
212 | headers
213 | };
214 |
215 | await octokit.graphql(mutation, parameters);
216 | }
217 |
218 | (async function main() {
219 | // Get project items
220 |
221 | console.log(`Querying value for field "${FIELD_NAME}" of ${ISSUE_URL}`);
222 | const projectItems = await queryFieldValue(REPO_OWNER, REPO_NAME, ISSUE_NUMBER, FIELD_NAME);
223 |
224 | if (!projectItems) {
225 | console.log("Aborting because issue is not part of any projects");
226 | return;
227 | }
228 |
229 | // Determine field value
230 |
231 | fieldValue = determineFieldValue(projectItems, PROJECT_ID, FIELD_ID);
232 |
233 | if (!fieldValue.name || !fieldValue.id) {
234 | console.log("Aborting because issue is not part of the correct project");
235 | return;
236 | }
237 |
238 | console.log(`Determined field value "${fieldValue.name}" (${fieldValue.id})`);
239 |
240 | // Set field value on tracked issues
241 |
242 | await setFieldValueOnTrackedIssues(REPO_OWNER, REPO_NAME, ISSUE_URL, ISSUE_NUMBER, PROJECT_ID, FIELD_ID, FIELD_NAME, fieldValue);
243 |
244 | })();
245 |
--------------------------------------------------------------------------------
/.github/actions/octonode.d.ts:
--------------------------------------------------------------------------------
1 | declare module "octonode" {
2 |
3 | export function client(accessToken: string): Client
4 |
5 | export class Client {
6 | public getAsync(path: `/repos/${string}/labels`, params: { page: number, per_page: number }): Promise<[number, [Label]]>
7 | }
8 |
9 | export interface Label {
10 | name: string;
11 | description: string;
12 | color: string;
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/.github/actions/score-triaged-defects.mjs:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2024 New Vector Ltd.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 |
18 | // For running in an action
19 | import { Octokit } from "@octokit/action";
20 | const octokit = new Octokit();
21 |
22 | // For running locally
23 | // import { Octokit } from "@octokit/core";
24 | // const octokit = new Octokit({ auth: process.env.GH_TOKEN });
25 |
26 |
27 | // List all the defects with their GH project "Score" field
28 | async function listDefects(repoOwner, repoName, projectFieldName = "Score", label = "T-Defect") {
29 | const query = `
30 | query ($repoOwner: String!, $repoName: String!, $label: String!, $projectFieldName: String!, $after: String) {
31 | repository(owner: $repoOwner, name: $repoName) {
32 | issues(labels: [$label], states: OPEN, first: 10, after: $after) {
33 | nodes {
34 | number
35 | title
36 | labels(first: 10) {
37 | nodes {
38 | name
39 | }
40 | }
41 | projectItems(first: 10) {
42 | nodes {
43 | id
44 | project {
45 | id
46 | }
47 | score: fieldValueByName(name: $projectFieldName) {
48 | ... on ProjectV2ItemFieldNumberValue {
49 | id
50 | number
51 | }
52 | }
53 | }
54 | }
55 | }
56 | pageInfo {
57 | endCursor
58 | hasNextPage
59 | }
60 | }
61 | }
62 | }
63 | `;
64 |
65 | var issues = [];
66 | var hasNextPage = true;
67 | var after = null;
68 |
69 | while (hasNextPage) {
70 | const parameters = {
71 | repoOwner,
72 | repoName,
73 | label,
74 | projectFieldName,
75 | after
76 | };
77 |
78 | const result = await octokit.graphql(query, parameters);
79 | issues = issues.concat(result.repository.issues.nodes);
80 | hasNextPage = result.repository.issues.pageInfo.hasNextPage;
81 | after = result.repository.issues.pageInfo.endCursor;
82 | }
83 |
84 | return issues;
85 | }
86 |
87 |
88 | // Extract the score from the GraphQL response.
89 | // scoreItem is { id, project { id }, score: { id, number }}
90 | function getScoreItem(issue, projectId) {
91 | var scoreItem = 0;
92 | issue.projectItems.nodes.forEach(item => {
93 | if (item.project.id === projectId) {
94 | scoreItem = item;
95 | }
96 | });
97 |
98 | if (scoreItem == null) {
99 | console.log("No score found for issue " + issue.number);
100 | }
101 |
102 | return scoreItem;
103 | }
104 |
105 | // Compute the score of a defect based on the labels as per:
106 | // https://github.com/element-hq/element-meta/wiki/triage-process#prioritisation
107 | function computeIssueScore(issue) {
108 | var severity = 0;
109 | var occurence = 0;
110 | issue.labels.nodes.forEach(label => {
111 | switch (String(label.name)) {
112 | case "O-Uncommon":
113 | occurence = 1;
114 | break;
115 | case "O-Occasional":
116 | occurence = 2;
117 | break;
118 | case "O-Frequent":
119 | occurence = 3;
120 | break;
121 | case "S-Tolerable":
122 | severity = 1;
123 | break;
124 | case "S-Minor":
125 | severity = 2;
126 | break;
127 | case "S-Major":
128 | severity = 3;
129 | break;
130 | case "S-Critical":
131 | severity = 4;
132 | break;
133 | default:
134 | break;
135 | }
136 | });
137 |
138 | return severity * occurence;
139 | }
140 |
141 | // Update a score in the GH project
142 | async function setNewScore(scoreItem, projectFieldId, fieldValue) {
143 | const mutation = `
144 | mutation($projectId: ID!, $itemId: ID!, $projectFieldId: ID!, $value: Float!) {
145 | updateProjectV2ItemFieldValue(
146 | input: {
147 | projectId: $projectId
148 | itemId: $itemId
149 | fieldId: $projectFieldId
150 | value: {
151 | number: $value
152 | }
153 | }
154 | ) {
155 | projectV2Item {
156 | id
157 | }
158 | }
159 | }
160 | `;
161 |
162 | const parameters = {
163 | projectId: scoreItem.project.id,
164 | itemId: scoreItem.id,
165 | projectFieldId: projectFieldId,
166 | value: fieldValue
167 | };
168 |
169 | await octokit.graphql(mutation, parameters);
170 | }
171 |
172 |
173 | (async function main() {
174 | const repoOwner = process.env.REPO_OWNER;
175 | const repoName = process.env.REPO_NAME;
176 | const projectId = process.env.PROJECT_ID;
177 | const projectFieldId = process.env.PROJECT_FIELD_ID;
178 | const projectFieldName = process.env.PROJECT_FIELD_NAME;
179 |
180 | const issues = await listDefects(repoOwner, repoName, projectFieldName);
181 | console.log("Found " + issues.length + " T-Defect issues");
182 |
183 | issues.filter(issue => {
184 | // Check if it is part of the GH project
185 | const ok = issue.projectItems.nodes.some(item => { return item.project.id === projectId; });
186 | if (!ok) {
187 | console.log("Issue " + issue.number + " is not part of the project");
188 | }
189 | return ok;
190 | })
191 | .filter(issue => {
192 | // Check if it has the triaging labels, ie a label that starts with "S-" and a label that starts with "O-"
193 | const ok = issue.labels.nodes.some(label => { return label.name.startsWith("S-"); }) && issue.labels.nodes.some(label => { return label.name.startsWith("O-"); });
194 | if (!ok) {
195 | console.log("Issue " + issue.number + " is not labeled correctly. Labels: " + issue.labels.nodes.map(label => label.name));
196 | }
197 | return ok;
198 | })
199 | .forEach(issue => {
200 | const scoreItem = getScoreItem(issue, projectId);
201 |
202 | // Ignore issues with a score manually set higher than 100. This is a way to fine control the priority of issues
203 | if (scoreItem.score && scoreItem.score.number >= 100) {
204 | return;
205 | }
206 |
207 | // Update the score if it is different
208 | var computedScore = computeIssueScore(issue);
209 | if (scoreItem.score == null || scoreItem.score.number != computedScore) {
210 | console.log(issue.number + " - " + " Updating score from " + (scoreItem.score ? scoreItem.score.number : "null") + " to " + computedScore + " - " + issue.title);
211 |
212 | setNewScore(scoreItem, projectFieldId, computedScore).catch(error => {
213 | console.error("Error updating score for issue " + issue.number + ": " + error);
214 | });
215 | }
216 | });
217 |
218 | })();
219 |
220 |
221 |
222 | // The query to use in https://docs.github.com/en/graphql/overview/explorer to find ids for the GH project id and the Score field
223 | /*
224 | query($owner: String!, $repo: String!) {
225 | repository(owner: $owner, name: $repo) {
226 | issues(labels: ["T-Defect"], states: OPEN, first:3) {
227 | nodes {
228 | title
229 | projectItems(first: 2) {
230 | nodes {
231 | project {
232 | id
233 | score: field(name: "Score") {
234 | ... on ProjectV2Field {
235 | id
236 | }
237 | }
238 | }
239 | }
240 | }
241 | }
242 | }
243 | }
244 | }
245 |
246 | Variables
247 | {"owner": "element-hq","repo": "element-x-ios"}
248 | */
249 |
--------------------------------------------------------------------------------
/.github/actions/sync-labels.ts:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2023 New Vector Ltd.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | import fs from "fs";
18 | import { Command } from "commander";
19 | import githubLabelSync, { LabelDiff, LabelInfo } from "github-label-sync";
20 | import octonode from "octonode";
21 | import YAML from "yaml";
22 |
23 | main();
24 |
25 | async function main() {
26 | const accessToken = process.env.GITHUB_TOKEN!;
27 | const repo = process.env.GITHUB_REPOSITORY!;
28 | console.log(`Repository: ${repo}`);
29 |
30 | const cmd = new Command("sync-labels.ts")
31 | .option("--delete", "Removes labels that exist in the repository but are missing from all sources", false)
32 | .option("--dir ", "Working directory to run in")
33 | .option("--labels ", "Label source as GitHub repository slug or path to local YAML file. Can be specified multiple times.")
34 | .option("--wet", "Write changes, *don't* run in dry mode", false)
35 | .parse();
36 | const opts = cmd.opts();
37 | console.log(`Options: ${JSON.stringify(opts)}`);
38 |
39 | if (opts.dir) {
40 | process.chdir(opts.dir);
41 | }
42 |
43 | try {
44 | const merged = await readAndMergeLabels(opts.labels, accessToken);
45 | await syncLabels(merged, repo, accessToken, opts.delete, opts.wet);
46 | } catch (e) {
47 | console.error("Error syncing labels", e);
48 | process.exit(1);
49 | }
50 | }
51 |
52 | async function readAndMergeLabels(srcs: string[], accessToken: string): Promise