From 884fd37c83c8729035cd5d557909b104bbebb4a6 Mon Sep 17 00:00:00 2001 From: Jakob Erben <34318568+erbenjak@users.noreply.github.com> Date: Wed, 3 Jan 2024 12:00:30 +0100 Subject: [PATCH] Feature/publish-command-teams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: version bump * feature: create teams publish command * feature: initial teams publish command * feature: extend teams-build command * refactor: minor reordering * refactor: improve parameter names * refactor: change build-command text slightly to better differentiate between the two types of messages * feature: refine teams-publish command * refactor: format project * refactor: improve output message * refactor: reformat project * chore: complete version bump-up * feature: remove artifact link from publish message * refactor: rename jscm sub-commands * refactor: rename to match command-names * refactor: reformat files * feature: improve message quality * wip * chore: add test artifact-links * feature: adapt command to use environment variables * chore: move isUrlValid to utility * chore: simplify everything * refactor: use semis * refactor: use functions instead of classes * test: add tests for teams messaging * chore: formatting * chore: formatting * refactor: use correct artifact links file in testing * feature: improve artifact type check --------- Co-authored-by: Julian König --- .prettierrc | 2 +- README.md | 42 +++++++- package-lock.json | 4 +- package.json | 2 +- src/CmArtifactLink.ts | 8 ++ src/commands/TeamsCommandLineOptions.ts | 4 + src/commands/TeamsDevelopMessaging.ts | 57 +++++++++++ src/commands/TeamsMessaging.ts | 81 ---------------- src/commands/TeamsProductionMessaging.ts | 43 +++++++++ src/commands/isUrlValid.ts | 9 ++ src/index.ts | 116 ++++++++++++++++++++--- test/artifactLinks.json | 16 ++++ test/test_teams_messaging.sh | 31 ++++++ 13 files changed, 317 insertions(+), 98 deletions(-) create mode 100644 src/CmArtifactLink.ts create mode 100644 src/commands/TeamsCommandLineOptions.ts create mode 100644 src/commands/TeamsDevelopMessaging.ts delete mode 100644 src/commands/TeamsMessaging.ts create mode 100644 src/commands/TeamsProductionMessaging.ts create mode 100644 src/commands/isUrlValid.ts create mode 100644 test/artifactLinks.json create mode 100644 test/test_teams_messaging.sh diff --git a/.prettierrc b/.prettierrc index 3693121..1f1a17a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,5 @@ "printWidth": 120, "tabWidth": 2, "trailingComma": "none", - "semi": false + "semi": true } diff --git a/README.md b/README.md index 462a264..14157ba 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,48 @@ npm install @js-soft/codemagic-tools ## Usage +### Teams Messaging + +#### Develop Messages + +```bash +jscm teams-develop --platform --projectName +``` + +This command will inform about a new development build and additionally provide a link to the build. In case of a failed build it will also provide a link to the build log. + +### Production Messages + +```bash +jscm teams-publish --platform --projectName +``` + +This command will inform about an app version, that was released in a store. It additionally provides a link to the build logs. + +## Testing + +For testing a JSON like created in Codemagic is provided. Additionally a bash-script, which +can be used to test the command is provided. Upon execution the +test script will ask you to specify the following variables: + +- webhook - webhook url you want to send to / or just some valid https-address +- BuildId - a string +- ProjectId - a string +- buildNumber - a number + +After preparation of your local environment the script will execute the jscm command. + +It will execute both possible command: + +- teams-develop (in failed/successful state) +- teams-production (in failed/successful state) + +→ This will result in 4 messages being sent to the specified teams channel + +### Calling the test script + ```bash -jscm --help +./test/test_teams_messaging.sh ``` ## License diff --git a/package-lock.json b/package-lock.json index 582e54c..26e3b55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@js-soft/codemagic-tools", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@js-soft/codemagic-tools", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "dependencies": { "axios": "^1.6.2", diff --git a/package.json b/package.json index ad3b101..d423507 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@js-soft/codemagic-tools", - "version": "0.0.2", + "version": "0.0.3", "description": "Codemagic extended tooling", "homepage": "https://github.com/js-soft/codemagic-tools#readme", "bugs": { diff --git a/src/CmArtifactLink.ts b/src/CmArtifactLink.ts new file mode 100644 index 0000000..2c3bcd2 --- /dev/null +++ b/src/CmArtifactLink.ts @@ -0,0 +1,8 @@ +export interface CmArtifactLink { + name: string; + type: string; + url: string; + md5: string; + versionName: string; + bundleId: string; +} diff --git a/src/commands/TeamsCommandLineOptions.ts b/src/commands/TeamsCommandLineOptions.ts new file mode 100644 index 0000000..54a6663 --- /dev/null +++ b/src/commands/TeamsCommandLineOptions.ts @@ -0,0 +1,4 @@ +export interface TeamsCommandLineOptions { + platform: string; + projectName: string; +} diff --git a/src/commands/TeamsDevelopMessaging.ts b/src/commands/TeamsDevelopMessaging.ts new file mode 100644 index 0000000..c8314eb --- /dev/null +++ b/src/commands/TeamsDevelopMessaging.ts @@ -0,0 +1,57 @@ +import axios from "axios"; +import { isUrlValid } from "./isUrlValid"; + +export interface TeamsDevelopMessagingOptions { + projectName: string; + webhook: string; + artifactUrl: string; + buildUrl: string; + buildWasSuccessful: boolean; + platform: string; + buildNumber: number; +} + +export async function runTeamsDevelopMessagingCommand(options: TeamsDevelopMessagingOptions): Promise { + if (!isUrlValid(options.webhook)) { + console.error("The given webhook is not valid."); + process.exit(1); + } + + if (!isUrlValid(options.artifactUrl)) { + console.error("The given artifactUrl is not valid."); + process.exit(1); + } + + if (!isUrlValid(options.buildUrl)) { + console.error("The given buildUrl is not valid."); + process.exit(1); + } + + const statusIdentifier = options.buildWasSuccessful ? "Successful" : "Failed"; + const platformIdentifier = options.platform.toUpperCase(); + + const messageContents = { + title: `New ${statusIdentifier.toLowerCase()} ${options.platform} debug build for the "${options.projectName}" App`, + summary: `${options.projectName}: ${statusIdentifier} build - ${platformIdentifier}`, + text: options.buildWasSuccessful + ? `New Build: #${options.buildNumber} - ${platformIdentifier}
The latest version build successfully and is now available as an artifact.` + : `New Build: #${options.buildNumber} - ${platformIdentifier}
A problem occurred while building the newly released version. The corresponding logs are available.`, + potentialAction: [ + { + "@type": "OpenUri", + name: options.buildWasSuccessful ? `Download ${options.platform}-App` : "Download Flutter Logs", + targets: [{ os: "default", uri: options.artifactUrl }] + }, + { + "@type": "OpenUri", + name: "Open Build", + targets: [{ os: "default", uri: options.buildUrl }] + } + ] + }; + + await axios.post(options.webhook, messageContents).catch((_) => { + console.log("Could not send message to teams channel."); + process.exit(1); + }); +} diff --git a/src/commands/TeamsMessaging.ts b/src/commands/TeamsMessaging.ts deleted file mode 100644 index c52d4a4..0000000 --- a/src/commands/TeamsMessaging.ts +++ /dev/null @@ -1,81 +0,0 @@ -import axios from "axios" -import yargs from "yargs" - -export interface TeamsMessagingOptions { - webhook: string - platform: string - artifactUrl: string - wasBuildSuccessful: boolean -} - -export class TeamsMessaging { - public async run(options: TeamsMessagingOptions): Promise { - if (!this.isUrlValid(options.webhook)) { - console.error("The given webhook is not valid.") - process.exit(1) - } - - if (!this.isUrlValid(options.artifactUrl)) { - console.error("The given artifactUrl is not valid.") - process.exit(1) - } - - const statusIdentifier = options.wasBuildSuccessful ? "Successful" : "Failed" - const platformIdentifier = options.platform.toUpperCase() - - const messageContents = { - title: `New ${statusIdentifier.toLocaleLowerCase()} codemagic build - ${platformIdentifier}`, - summary: `${statusIdentifier} build - ${platformIdentifier}`, - text: options.wasBuildSuccessful - ? "The newly released version did build and is now available as an artifact." - : "A problem occurred while building the newly released version. The corresponding logs are available.", - potentialAction: [ - { - "@type": "OpenUri", - name: options.wasBuildSuccessful ? "Download Build" : "Read Logs", - targets: [{ os: "default", uri: options.artifactUrl }] - } - ] - } - - await axios.post(options.webhook, messageContents).catch((_) => { - console.log("Could not send message to teams channel.") - process.exit(1) - }) - } - - public parseCLIOptions(argv: yargs.Argv<{}>): TeamsMessagingOptions | Promise { - return argv - .option("platform", { - description: "identifier of the platform for which the build was created", - required: true, - type: "string", - choices: ["ios", "android"] - }) - .option("artifactUrl", { - description: "download link for the generated artifact (logs or build)", - required: true, - type: "string" - }) - .option("wasBuildSuccessful", { - description: "status of the finished build", - required: true, - type: "boolean" - }) - .option("webhook", { - description: "the webhook of the teams channel, that should receive the message", - required: true, - type: "string" - }).argv - } - - private isUrlValid(url: string): boolean { - try { - // eslint-disable-next-line no-new - new URL(url) - return true - } catch (err) { - return false - } - } -} diff --git a/src/commands/TeamsProductionMessaging.ts b/src/commands/TeamsProductionMessaging.ts new file mode 100644 index 0000000..3ab08ad --- /dev/null +++ b/src/commands/TeamsProductionMessaging.ts @@ -0,0 +1,43 @@ +import axios from "axios"; +import { isUrlValid } from "./isUrlValid"; + +export interface TeamsProductionMessagingOptions { + projectName: string; + platform: string; + buildUrl: string; + buildNumber: number; + webhook: string; +} + +export async function runTeamsProductionMessagingCommand(options: TeamsProductionMessagingOptions): Promise { + if (!isUrlValid(options.webhook)) { + console.error("The given webhook is not valid."); + process.exit(1); + } + + if (!isUrlValid(options.buildUrl)) { + console.error("The given buildUrl is not valid."); + process.exit(1); + } + + const platformIdentifier = options.platform.toUpperCase(); + const storeName = platformIdentifier === "IOS" ? "App Store" : "Google Play Store"; + const messageContents = { + title: `${options.projectName}: New release is now available in the ${storeName} [${platformIdentifier}]`, + summary: `New Release - ${platformIdentifier}`, + text: `New Release: #${options.buildNumber} - ${platformIdentifier}
+ The newly released version is now available in the ${storeName}. `, + potentialAction: [ + { + "@type": "OpenUri", + name: "Open Build", + targets: [{ os: "default", uri: options.buildUrl }] + } + ] + }; + + await axios.post(options.webhook, messageContents).catch((_) => { + console.log("Could not send message to teams channel."); + process.exit(1); + }); +} diff --git a/src/commands/isUrlValid.ts b/src/commands/isUrlValid.ts new file mode 100644 index 0000000..adf028e --- /dev/null +++ b/src/commands/isUrlValid.ts @@ -0,0 +1,9 @@ +export function isUrlValid(url: string): boolean { + try { + // eslint-disable-next-line no-new + new URL(url); + return true; + } catch (err) { + return false; + } +} diff --git a/src/index.ts b/src/index.ts index 89123cb..2db3939 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,24 +1,116 @@ #!/usr/bin/env node -import yargs from "yargs" -import { TeamsMessaging } from "./commands/TeamsMessaging" +import fs from "fs"; +import yargs from "yargs"; +import { CmArtifactLink } from "./CmArtifactLink"; +import { TeamsCommandLineOptions } from "./commands/TeamsCommandLineOptions"; +import { runTeamsDevelopMessagingCommand } from "./commands/TeamsDevelopMessaging"; +import { runTeamsProductionMessagingCommand } from "./commands/TeamsProductionMessaging"; async function run() { + const buildWasSuccessful = fs.existsSync("~/SUCCESS"); + + const webhook = process.env.teams_webhook_url!; + const buildId = process.env.CM_BUILD_ID!; + const projectId = process.env.CM_PROJECT_ID!; + const buildNumber = parseInt(process.env.BUILD_NUMBER!); + const buildUrl = `https://codemagic.io/app/${projectId}/build/${buildId}`; + + if (process.env.CM_ARTIFACT_LINKS === undefined) { + console.error( + "To ensure correct execution the environment variable CM_ARTIFACT_LINKS must be present in the system" + ); + process.exit(1); + } + + let artifactUrl: string; + let artifactType: string; + + const cmArtifactLinks: CmArtifactLink[] = JSON.parse(process.env.CM_ARTIFACT_LINKS!).filter( + (element: any) => element.type === "apk" || element.type === "ipa" + ); + if (cmArtifactLinks.filter((element: any) => element.type === "apk" || element.type === "ipa").length !== 0) { + const pickedElement = cmArtifactLinks.filter((element: any) => element.type === "apk" || element.type === "ipa")[0]; + artifactUrl = pickedElement.url; + artifactType = pickedElement.type; + } else { + // should link to the workflow-log can be determined from buildId and projectId + artifactUrl = buildUrl; + artifactType = "logs"; + } + await yargs(process.argv.slice(2)) - .command("teams", "This command is used to send a teams message via a passed webhook", async (args) => { - const teamsMessagingCommand = new TeamsMessaging() - const options = await teamsMessagingCommand.parseCLIOptions(args) - await teamsMessagingCommand.run(options) - return options + .option("platform", { + description: "Identifier of the platform for which the build was created", + required: true, + type: "string", + choices: ["ios", "android"] }) - .demand(1, "Must provide a valid command from the ones listed above.") + .option("projectName", { + description: "Name of the project", + required: true, + type: "string" + }) + .command( + "teams-develop", + "After Codemagic Build: Send MS-Teams message informing about the new build", + // empty function to avoid yargs internal error + () => { + return; + }, + async (args) => { + checkArtifactLinkMatchesPlatform(args, artifactType); + + await runTeamsDevelopMessagingCommand({ + projectName: args.projectName, + webhook, + artifactUrl, + buildUrl, + buildWasSuccessful, + platform: args.platform, + buildNumber + }); + } + ) + .command( + "teams-production", + "After Codemagic Publish: Send MS-Teams message informing about the new release", + // empty function to avoid yargs internal error + () => { + return; + }, + async (args) => + await runTeamsProductionMessagingCommand({ + projectName: args.projectName, + platform: args.platform, + buildUrl, + buildNumber, + webhook + }) + ) + .demandCommand(1, "Must provide a valid command from the ones listed above.") .scriptName("jscm") - .parseAsync() + .parseAsync(); +} + +function checkArtifactLinkMatchesPlatform(resolvedOptions: TeamsCommandLineOptions, artifactType: string) { + // throw exception if the artifact link does not have the correct type for the given platform + if (artifactType === "logs") { + return; + } + + if ( + (resolvedOptions.platform === "ios" && artifactType === "apk") || + (resolvedOptions.platform === "android" && artifactType === "ipa") + ) { + console.log("The artifact link does not have the correct type for the given platform"); + process.exit(1); + } } run() .then(() => process.exit(0)) .catch((error) => { - console.error(error) - process.exit(1) - }) + console.error(error); + process.exit(1); + }); diff --git a/test/artifactLinks.json b/test/artifactLinks.json new file mode 100644 index 0000000..5925173 --- /dev/null +++ b/test/artifactLinks.json @@ -0,0 +1,16 @@ +[ + { + "name": "Codemagic_Release.ipa", + "type": "ipa", + "url": "https://www.test.com", + "md5": "11111111111111111111111111111111", + "versionName": "1.0.0", + "bundleId": "io.codemagic.app" + }, + { + "name": "logs.txt", + "type": "txt", + "url": "https://www.test.com/logs.txt", + "md5": "00000000000000000000000000000000" + } +] diff --git a/test/test_teams_messaging.sh b/test/test_teams_messaging.sh new file mode 100644 index 0000000..bb914b6 --- /dev/null +++ b/test/test_teams_messaging.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +#this surely is not the best way to do it, one should use a .enc file which is loaded +echo "Enter a teams_webhook_url: " +read teams_webhook_url_value +export teams_webhook_url=$teams_webhook_url_value + +echo "Enter the CM_BUILD_ID : " +read CM_BUILD_ID_VALUE +export CM_BUILD_ID=$CM_BUILD_ID_VALUE + +echo "Enter the CM_PROJECT_ID : " +read CM_PROJECT_ID_VALUE +export CM_PROJECT_ID=$CM_PROJECT_ID_VALUE + +echo "Enter the BUILD_NUMBER :" +read BUILD_NUMBER_VALUE +export BUILD_NUMBER=$BUILD_NUMBER_VALUE + +export CM_ARTIFACT_LINKS=$(cat artifactLinks.json) + +echo "webhook url: $teams_webhook_url" + +echo "Starting teams messaging tests" +jscm teams-develop --platform "ios" --projectName "Mein Codemagic Test Projekt" +jscm teams-production --platform "ios" --projectName "Mein Codemagic Test Projekt" +touch ~/SUCCESS +jscm teams-develop --platform "ios" --projectName "Mein Codemagic Test Projekt" +jscm teams-production --platform "ios" --projectName "Mein Codemagic Test Projekt" +rm -f ~/SUCCESS +echo "Finished teams messaging tests" \ No newline at end of file