Skip to content

Commit 3f937de

Browse files
authored
Automation utils improvements (#1770)
2 parents 71a789b + 7987de2 commit 3f937de

File tree

13 files changed

+751
-42
lines changed

13 files changed

+751
-42
lines changed

.github/workflows/PublishMarketplace.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
name: "Publish a new package version from GitHub release"
1515
runs-on: ubuntu-latest
1616
env:
17-
TAG: ${{ github.event_name == 'release' && github.ref_name || github.event.inputs.package }}
17+
TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.package }}
1818

1919
steps:
2020
- name: Check release tag

automation/utils/bin/rui-prepare-release.ts

Lines changed: 454 additions & 0 deletions
Large diffs are not rendered by default.

automation/utils/bin/rui-publish-marketplace.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import assert from "node:assert/strict";
44
import { getPublishedInfo, gh } from "../src";
5-
import { fgGreen } from "../src/ansi-colors";
65
import { createDraft, publishDraft } from "../src/api/contributor";
6+
import chalk from "chalk";
77

88
async function main(): Promise<void> {
99
console.log(`Getting package information...`);
@@ -13,11 +13,11 @@ async function main(): Promise<void> {
1313
assert.ok(tag, "env.TAG is empty");
1414

1515
if (marketplace.appNumber === -1) {
16-
console.log(`Skipping release process for tag ${fgGreen(tag)}. appNumber is set to -1 in package.json.`);
16+
console.log(`Skipping release process for tag ${chalk.green(tag)}. appNumber is set to -1 in package.json.`);
1717
process.exit(2);
1818
}
1919

20-
console.log(`Starting release process for tag ${fgGreen(tag)}`);
20+
console.log(`Starting release process for tag ${chalk.green(tag)}`);
2121

2222
const artifactUrl = await gh.getMPKReleaseArtifactUrl(tag);
2323

automation/utils/bin/rui-verify-package-format.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22

33
import { ZodError } from "zod";
44
import {
5+
getModuleChangelog,
56
getPackageFileContent,
6-
PackageSchema,
7-
ModulePackageSchema,
7+
getWidgetChangelog,
88
JSActionsPackageSchema,
9+
ModulePackageSchema,
10+
PackageSchema,
911
PublishedPackageSchema
1012
} from "../src";
1113
import { verify as verifyWidget } from "../src/verify-widget-manifest";
12-
import { fgCyan, fgGreen, fgYellow } from "../src/ansi-colors";
13-
import { getModuleChangelog, getWidgetChangelog } from "../src";
14+
import chalk from "chalk";
1415

1516
async function main(): Promise<void> {
1617
const path = process.cwd();
@@ -63,13 +64,13 @@ async function main(): Promise<void> {
6364

6465
// Changelog check coming soon...
6566

66-
console.log(fgGreen("Verification success"));
67+
console.log(chalk.green("Verification success"));
6768
} catch (error) {
6869
if (error instanceof ZodError) {
6970
for (const issue of error.issues) {
70-
const keys = issue.path.map(x => fgYellow(`${x}`));
71+
const keys = issue.path.map(x => chalk.yellow(`${x}`));
7172
const code = `[${issue.code}]`;
72-
console.error(`package.${keys.join(".")} - ${code} ${fgCyan(issue.message)}`);
73+
console.error(`package.${keys.join(".")} - ${code} ${chalk.cyan(issue.message)}`);
7374
}
7475
// Just for new line
7576
console.log("");

automation/utils/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"rui-agent-rules": "bin/rui-agent-rules.ts",
99
"rui-create-gh-release": "bin/rui-create-gh-release.ts",
1010
"rui-create-translation": "bin/rui-create-translation.ts",
11+
"rui-prepare-release": "bin/rui-prepare-release.ts",
1112
"rui-publish-marketplace": "bin/rui-publish-marketplace.ts",
1213
"rui-update-changelog-module": "bin/rui-update-changelog-module.ts",
1314
"rui-update-changelog-widget": "bin/rui-update-changelog-widget.ts",
@@ -29,6 +30,7 @@
2930
"format": "prettier --write .",
3031
"lint": "eslint --ext .jsx,.js,.ts,.tsx src/",
3132
"prepare": "pnpm run compile:parser:widget && pnpm run compile:parser:module && tsc",
33+
"prepare-release": "ts-node bin/rui-prepare-release.ts",
3234
"start": "tsc --watch",
3335
"version": "ts-node bin/rui-bump-version.ts"
3436
},
@@ -37,7 +39,7 @@
3739
"@mendix/prettier-config-web-widgets": "workspace:*",
3840
"@types/cross-zip": "^4.0.2",
3941
"@types/node-fetch": "2.6.12",
40-
"chalk": "^4.1.2",
42+
"chalk": "^5.4.1",
4143
"cross-zip": "^4.0.1",
4244
"enquirer": "^2.4.1",
4345
"execa": "^5.1.1",

automation/utils/src/ansi-colors.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

automation/utils/src/api/contributor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import assert from "node:assert/strict";
2-
import { fetch, BodyInit } from "../fetch";
2+
import { BodyInit, fetch } from "../fetch";
33
import { z } from "zod";
44
import { Version } from "../version";
5-
import { fgGreen } from "../ansi-colors";
5+
import chalk from "chalk";
66

77
export interface CreateDraftSuccessResponse {
88
App: App;
@@ -115,7 +115,7 @@ export async function createDraft(params: CreateDraftParams): Promise<CreateDraf
115115
const { appName, appNumber, version, studioProVersion, artifactUrl, reactReady } = CreateDraftParams.parse(params);
116116
console.log(`Creating draft in the Mendix Marketplace...`);
117117
console.log(
118-
fgGreen(
118+
chalk.green(
119119
`AppName: ${appName} - AppNumber: ${appNumber} - Version: ${version.format()} - StudioPro: ${studioProVersion.format()}`
120120
)
121121
);

automation/utils/src/build-config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { join } from "node:path";
2-
import { fgGreen } from "./ansi-colors";
2+
import chalk from "chalk";
33
import { getModuleInfo, getWidgetInfo, ModuleInfo, WidgetInfo } from "./package-info";
44

55
export interface Output<Dirs, Files> {
@@ -65,7 +65,7 @@ export async function getWidgetBuildConfig({
6565
console.info(`Creating build config for ${packageName}...`);
6666

6767
if (MX_PROJECT_PATH) {
68-
console.info(fgGreen(`targetProject: using project path from MX_PROJECT_PATH.`));
68+
console.info(chalk.green(`targetProject: using project path from MX_PROJECT_PATH.`));
6969
}
7070

7171
const paths = {
@@ -118,7 +118,7 @@ export async function getModuleBuildConfig({
118118
console.info(`Creating build config for ${packageName}...`);
119119

120120
if (MX_PROJECT_PATH) {
121-
console.info(fgGreen(`targetProject: using project path from MX_PROJECT_PATH.`));
121+
console.info(chalk.green(`targetProject: using project path from MX_PROJECT_PATH.`));
122122
}
123123

124124
const paths = {

automation/utils/src/github.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,46 @@ export class GitHub {
147147

148148
return filePath;
149149
}
150+
151+
private async triggerGithubWorkflow(params: {
152+
workflowId: string;
153+
ref: string;
154+
inputs: Record<string, string>;
155+
owner?: string;
156+
repo?: string;
157+
}): Promise<void> {
158+
await this.ensureAuth();
159+
160+
const { workflowId, ref, inputs, owner = "mendix", repo = "web-widgets" } = params;
161+
162+
// Convert inputs object to CLI parameters
163+
const inputParams = Object.entries(inputs)
164+
.map(([key, value]) => `-f ${key}=${value}`)
165+
.join(" ");
166+
167+
const repoParam = `${owner}/${repo}`;
168+
169+
const command = [`gh workflow run`, `"${workflowId}"`, `--ref "${ref}"`, inputParams, `-R "${repoParam}"`]
170+
.filter(Boolean)
171+
.join(" ");
172+
173+
try {
174+
await exec(command);
175+
console.log(`Successfully triggered workflow '${workflowId}'`);
176+
} catch (error) {
177+
throw new Error(`Failed to trigger workflow '${workflowId}': ${error}`);
178+
}
179+
}
180+
181+
async triggerCreateReleaseWorkflow(packageName: string, ref = "main"): Promise<void> {
182+
return this.triggerGithubWorkflow({
183+
workflowId: "CreateGitHubRelease.yml",
184+
ref,
185+
inputs: {
186+
package: packageName
187+
}
188+
});
189+
}
150190
}
151191

152192
export const gh = new GitHub();

automation/utils/src/jira.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import nodefetch, { RequestInit } from "node-fetch";
2+
3+
interface JiraVersion {
4+
id: string;
5+
name: string;
6+
archived: boolean;
7+
released: boolean;
8+
}
9+
10+
interface JiraProject {
11+
id: string;
12+
key: string;
13+
name: string;
14+
}
15+
16+
interface JiraIssue {
17+
key: string;
18+
fields: {
19+
summary: string;
20+
};
21+
}
22+
23+
export class Jira {
24+
private projectKey: string;
25+
private baseUrl: string;
26+
private apiToken: string;
27+
28+
private projectId: string | undefined;
29+
private projectVersions: JiraVersion[] | undefined;
30+
31+
constructor(projectKey: string, baseUrl: string, apiToken: string) {
32+
if (!apiToken) {
33+
throw new Error("API token is required.");
34+
}
35+
this.projectKey = projectKey;
36+
this.baseUrl = baseUrl;
37+
38+
this.apiToken = Buffer.from(apiToken).toString("base64"); // Convert to Base64
39+
}
40+
41+
// Private helper method for making API requests
42+
private async apiRequest<T = unknown>(
43+
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
44+
endpoint: string,
45+
body?: object
46+
): Promise<T> {
47+
const url = `${this.baseUrl}/rest/api/3${endpoint}`;
48+
const headers = { Authorization: `Basic ${this.apiToken}` };
49+
50+
const httpsOptions: RequestInit = {
51+
method,
52+
redirect: "follow",
53+
headers: {
54+
Accept: "application/json",
55+
...headers,
56+
...(body && { "Content-Type": "application/json" })
57+
},
58+
body: body ? JSON.stringify(body) : undefined
59+
};
60+
61+
let response;
62+
try {
63+
response = await nodefetch(url, httpsOptions);
64+
} catch (error) {
65+
throw new Error(`API request failed: ${(error as Error).message}`);
66+
}
67+
68+
if (!response.ok) {
69+
throw new Error(`API request failed (${response.status}): ${response.statusText}`);
70+
}
71+
72+
if (response.status === 204) {
73+
// No content, return empty object
74+
return {} as T;
75+
}
76+
77+
return response.json();
78+
}
79+
80+
async initializeProjectData(): Promise<void> {
81+
const projectData = await this.apiRequest<JiraProject & { versions: JiraVersion[] }>(
82+
"GET",
83+
`/project/${this.projectKey}`
84+
);
85+
86+
this.projectId = projectData.id; // Save project ID
87+
this.projectVersions = projectData.versions.reverse(); // Save list of versions
88+
}
89+
90+
private versions(): JiraVersion[] {
91+
if (!this.projectVersions) {
92+
throw new Error("Project versions not initialized. Call initializeProjectData() first.");
93+
}
94+
return this.projectVersions;
95+
}
96+
97+
getVersions(): JiraVersion[] {
98+
return this.versions();
99+
}
100+
101+
findVersion(versionName: string): JiraVersion | undefined {
102+
return this.versions().find(version => version.name === versionName);
103+
}
104+
105+
async createVersion(name: string): Promise<JiraVersion> {
106+
const version = await this.apiRequest<JiraVersion>("POST", `/version`, {
107+
projectId: this.projectId,
108+
name
109+
});
110+
111+
this.projectVersions!.unshift(version);
112+
113+
return version;
114+
}
115+
116+
async assignVersionToIssue(versionId: string, issueKey: string): Promise<void> {
117+
await this.apiRequest("PUT", `/issue/${issueKey}`, {
118+
fields: {
119+
fixVersions: [{ id: versionId }]
120+
}
121+
});
122+
}
123+
124+
async deleteVersion(versionId: string): Promise<void> {
125+
await this.apiRequest("DELETE", `/version/${versionId}`);
126+
127+
// Remove the version from the cached project versions
128+
this.projectVersions = this.projectVersions?.filter(version => version.id !== versionId);
129+
}
130+
131+
async getFixVersionsForIssue(issueKey: string): Promise<JiraVersion[]> {
132+
const issue = await this.apiRequest<{ fields: { fixVersions: JiraVersion[] } }>(
133+
"GET",
134+
`/issue/${issueKey}?fields=fixVersions`
135+
);
136+
137+
return issue.fields.fixVersions || [];
138+
}
139+
140+
async removeFixVersionFromIssue(versionId: string, issueKey: string): Promise<void> {
141+
// First, get current fix versions
142+
const currentVersions = await this.getFixVersionsForIssue(issueKey);
143+
144+
// Filter out the version to remove
145+
const updatedVersions = currentVersions
146+
.filter(version => version.id !== versionId)
147+
.map(version => ({ id: version.id }));
148+
149+
// Update the issue with the filtered versions
150+
await this.apiRequest("PUT", `/issue/${issueKey}`, {
151+
fields: {
152+
fixVersions: updatedVersions
153+
}
154+
});
155+
}
156+
157+
private async getIssuesForVersion(versionId: string): Promise<string[]> {
158+
const issues = await this.apiRequest<{ issues: Array<{ key: string }> }>(
159+
"GET",
160+
`/search?jql=fixVersion=${versionId}`
161+
);
162+
163+
return issues.issues.map(issue => issue.key);
164+
}
165+
166+
async getIssuesWithDetailsForVersion(versionId: string): Promise<JiraIssue[]> {
167+
const response = await this.apiRequest<{ issues: JiraIssue[] }>(
168+
"GET",
169+
`/search?jql=fixVersion=${versionId}&fields=summary`
170+
);
171+
172+
return response.issues;
173+
}
174+
175+
async searchIssueByKey(issueKey: string): Promise<JiraIssue | null> {
176+
try {
177+
const issue = await this.apiRequest<JiraIssue>("GET", `/issue/${issueKey}?fields=summary`);
178+
return issue;
179+
} catch (_e) {
180+
// If issue not found or other error
181+
return null;
182+
}
183+
}
184+
}

0 commit comments

Comments
 (0)