Skip to content

Automation utils improvements #1770

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/PublishMarketplace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
name: "Publish a new package version from GitHub release"
runs-on: ubuntu-latest
env:
TAG: ${{ github.ref_name }}
TAG: ${{ github.event.release.tag_name }}

steps:
- name: Check release tag
Expand Down
454 changes: 454 additions & 0 deletions automation/utils/bin/rui-prepare-release.ts

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions automation/utils/bin/rui-publish-marketplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import assert from "node:assert/strict";
import { getPublishedInfo, gh } from "../src";
import { fgGreen } from "../src/ansi-colors";
import { createDraft, publishDraft } from "../src/api/contributor";
import chalk from "chalk";

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

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

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

const artifactUrl = await gh.getMPKReleaseArtifactUrl(tag);

Expand Down
15 changes: 8 additions & 7 deletions automation/utils/bin/rui-verify-package-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

import { ZodError } from "zod";
import {
getModuleChangelog,
getPackageFileContent,
PackageSchema,
ModulePackageSchema,
getWidgetChangelog,
JSActionsPackageSchema,
ModulePackageSchema,
PackageSchema,
PublishedPackageSchema
} from "../src";
import { verify as verifyWidget } from "../src/verify-widget-manifest";
import { fgCyan, fgGreen, fgYellow } from "../src/ansi-colors";
import { getModuleChangelog, getWidgetChangelog } from "../src";
import chalk from "chalk";

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

// Changelog check coming soon...

console.log(fgGreen("Verification success"));
console.log(chalk.green("Verification success"));
} catch (error) {
if (error instanceof ZodError) {
for (const issue of error.issues) {
const keys = issue.path.map(x => fgYellow(`${x}`));
const keys = issue.path.map(x => chalk.yellow(`${x}`));
const code = `[${issue.code}]`;
console.error(`package.${keys.join(".")} - ${code} ${fgCyan(issue.message)}`);
console.error(`package.${keys.join(".")} - ${code} ${chalk.cyan(issue.message)}`);
}
// Just for new line
console.log("");
Expand Down
4 changes: 3 additions & 1 deletion automation/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"rui-agent-rules": "bin/rui-agent-rules.ts",
"rui-create-gh-release": "bin/rui-create-gh-release.ts",
"rui-create-translation": "bin/rui-create-translation.ts",
"rui-prepare-release": "bin/rui-prepare-release.ts",
"rui-publish-marketplace": "bin/rui-publish-marketplace.ts",
"rui-update-changelog-module": "bin/rui-update-changelog-module.ts",
"rui-update-changelog-widget": "bin/rui-update-changelog-widget.ts",
Expand All @@ -29,6 +30,7 @@
"format": "prettier --write .",
"lint": "eslint --ext .jsx,.js,.ts,.tsx src/",
"prepare": "pnpm run compile:parser:widget && pnpm run compile:parser:module && tsc",
"prepare-release": "ts-node bin/rui-prepare-release.ts",
"start": "tsc --watch",
"version": "ts-node bin/rui-bump-version.ts"
},
Expand All @@ -37,7 +39,7 @@
"@mendix/prettier-config-web-widgets": "workspace:*",
"@types/cross-zip": "^4.0.2",
"@types/node-fetch": "2.6.12",
"chalk": "^4.1.2",
"chalk": "^5.4.1",
"cross-zip": "^4.0.1",
"enquirer": "^2.4.1",
"execa": "^5.1.1",
Expand Down
9 changes: 0 additions & 9 deletions automation/utils/src/ansi-colors.ts

This file was deleted.

6 changes: 3 additions & 3 deletions automation/utils/src/api/contributor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import assert from "node:assert/strict";
import { fetch, BodyInit } from "../fetch";
import { BodyInit, fetch } from "../fetch";
import { z } from "zod";
import { Version } from "../version";
import { fgGreen } from "../ansi-colors";
import chalk from "chalk";

export interface CreateDraftSuccessResponse {
App: App;
Expand Down Expand Up @@ -115,7 +115,7 @@ export async function createDraft(params: CreateDraftParams): Promise<CreateDraf
const { appName, appNumber, version, studioProVersion, artifactUrl, reactReady } = CreateDraftParams.parse(params);
console.log(`Creating draft in the Mendix Marketplace...`);
console.log(
fgGreen(
chalk.green(
`AppName: ${appName} - AppNumber: ${appNumber} - Version: ${version.format()} - StudioPro: ${studioProVersion.format()}`
)
);
Expand Down
6 changes: 3 additions & 3 deletions automation/utils/src/build-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { join } from "node:path";
import { fgGreen } from "./ansi-colors";
import chalk from "chalk";
import { getModuleInfo, getWidgetInfo, ModuleInfo, WidgetInfo } from "./package-info";

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

if (MX_PROJECT_PATH) {
console.info(fgGreen(`targetProject: using project path from MX_PROJECT_PATH.`));
console.info(chalk.green(`targetProject: using project path from MX_PROJECT_PATH.`));
}

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

if (MX_PROJECT_PATH) {
console.info(fgGreen(`targetProject: using project path from MX_PROJECT_PATH.`));
console.info(chalk.green(`targetProject: using project path from MX_PROJECT_PATH.`));
}

const paths = {
Expand Down
40 changes: 40 additions & 0 deletions automation/utils/src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,46 @@ export class GitHub {

return filePath;
}

private async triggerGithubWorkflow(params: {
workflowId: string;
ref: string;
inputs: Record<string, string>;
owner?: string;
repo?: string;
}): Promise<void> {
await this.ensureAuth();

const { workflowId, ref, inputs, owner = "mendix", repo = "web-widgets" } = params;

// Convert inputs object to CLI parameters
const inputParams = Object.entries(inputs)
.map(([key, value]) => `-f ${key}=${value}`)
.join(" ");

const repoParam = `${owner}/${repo}`;

const command = [`gh workflow run`, `"${workflowId}"`, `--ref "${ref}"`, inputParams, `-R "${repoParam}"`]
.filter(Boolean)
.join(" ");

try {
await exec(command);
console.log(`Successfully triggered workflow '${workflowId}'`);
} catch (error) {
throw new Error(`Failed to trigger workflow '${workflowId}': ${error}`);
}
}

async triggerCreateReleaseWorkflow(packageName: string, ref = "main"): Promise<void> {
return this.triggerGithubWorkflow({
workflowId: "CreateGitHubRelease.yml",
ref,
inputs: {
package: packageName
}
});
}
}

export const gh = new GitHub();
184 changes: 184 additions & 0 deletions automation/utils/src/jira.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import nodefetch, { RequestInit } from "node-fetch";

interface JiraVersion {
id: string;
name: string;
archived: boolean;
released: boolean;
}

interface JiraProject {
id: string;
key: string;
name: string;
}

interface JiraIssue {
key: string;
fields: {
summary: string;
};
}

export class Jira {
private projectKey: string;
private baseUrl: string;
private apiToken: string;

private projectId: string | undefined;
private projectVersions: JiraVersion[] | undefined;

constructor(projectKey: string, baseUrl: string, apiToken: string) {
if (!apiToken) {
throw new Error("API token is required.");
}
this.projectKey = projectKey;
this.baseUrl = baseUrl;

this.apiToken = Buffer.from(apiToken).toString("base64"); // Convert to Base64
}

// Private helper method for making API requests
private async apiRequest<T = unknown>(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
endpoint: string,
body?: object
): Promise<T> {
const url = `${this.baseUrl}/rest/api/3${endpoint}`;
const headers = { Authorization: `Basic ${this.apiToken}` };

const httpsOptions: RequestInit = {
method,
redirect: "follow",
headers: {
Accept: "application/json",
...headers,
...(body && { "Content-Type": "application/json" })
},
body: body ? JSON.stringify(body) : undefined
};

let response;
try {
response = await nodefetch(url, httpsOptions);
} catch (error) {
throw new Error(`API request failed: ${(error as Error).message}`);
}

if (!response.ok) {
throw new Error(`API request failed (${response.status}): ${response.statusText}`);
}

if (response.status === 204) {
// No content, return empty object
return {} as T;
}

return response.json();
}

async initializeProjectData(): Promise<void> {
const projectData = await this.apiRequest<JiraProject & { versions: JiraVersion[] }>(
"GET",
`/project/${this.projectKey}`
);

this.projectId = projectData.id; // Save project ID
this.projectVersions = projectData.versions.reverse(); // Save list of versions
}

private versions(): JiraVersion[] {
if (!this.projectVersions) {
throw new Error("Project versions not initialized. Call initializeProjectData() first.");
}
return this.projectVersions;
}

getVersions(): JiraVersion[] {
return this.versions();
}

findVersion(versionName: string): JiraVersion | undefined {
return this.versions().find(version => version.name === versionName);
}

async createVersion(name: string): Promise<JiraVersion> {
const version = await this.apiRequest<JiraVersion>("POST", `/version`, {
projectId: this.projectId,
name
});

this.projectVersions!.unshift(version);

return version;
}

async assignVersionToIssue(versionId: string, issueKey: string): Promise<void> {
await this.apiRequest("PUT", `/issue/${issueKey}`, {
fields: {
fixVersions: [{ id: versionId }]
}
});
}

async deleteVersion(versionId: string): Promise<void> {
await this.apiRequest("DELETE", `/version/${versionId}`);

// Remove the version from the cached project versions
this.projectVersions = this.projectVersions?.filter(version => version.id !== versionId);
}

async getFixVersionsForIssue(issueKey: string): Promise<JiraVersion[]> {
const issue = await this.apiRequest<{ fields: { fixVersions: JiraVersion[] } }>(
"GET",
`/issue/${issueKey}?fields=fixVersions`
);

return issue.fields.fixVersions || [];
}

async removeFixVersionFromIssue(versionId: string, issueKey: string): Promise<void> {
// First, get current fix versions
const currentVersions = await this.getFixVersionsForIssue(issueKey);

// Filter out the version to remove
const updatedVersions = currentVersions
.filter(version => version.id !== versionId)
.map(version => ({ id: version.id }));

// Update the issue with the filtered versions
await this.apiRequest("PUT", `/issue/${issueKey}`, {
fields: {
fixVersions: updatedVersions
}
});
}

private async getIssuesForVersion(versionId: string): Promise<string[]> {
const issues = await this.apiRequest<{ issues: Array<{ key: string }> }>(
"GET",
`/search?jql=fixVersion=${versionId}`
);

return issues.issues.map(issue => issue.key);
}

async getIssuesWithDetailsForVersion(versionId: string): Promise<JiraIssue[]> {
const response = await this.apiRequest<{ issues: JiraIssue[] }>(
"GET",
`/search?jql=fixVersion=${versionId}&fields=summary`
);

return response.issues;
}

async searchIssueByKey(issueKey: string): Promise<JiraIssue | null> {
try {
const issue = await this.apiRequest<JiraIssue>("GET", `/issue/${issueKey}?fields=summary`);
return issue;
} catch (_e) {
// If issue not found or other error
return null;
}
}
}
Loading
Loading