From 1ce2cf73ed502f4aadbee10e62333e5933f75c14 Mon Sep 17 00:00:00 2001 From: Gustav-Eikaas <89254170+Gustav-Eikaas@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:26:19 +0100 Subject: [PATCH] ci: migrate fusion app management v2 pipelines (#1165) --- .github/workflows/manual-deploy-prod.yml | 2 +- github-action/src/releaseMain.ts | 10 +++-- github-action/src/releasePr.ts | 21 +++++---- github-action/src/utils/bumpVersion.ts | 23 ++++++++++ github-action/src/utils/makeManifest.ts | 35 ++++----------- github-action/src/utils/patchAppConfig.ts | 4 +- github-action/src/utils/uploadBundle.ts | 53 ++++++++--------------- github-action/src/utils/zipBundle.ts | 5 +-- 8 files changed, 76 insertions(+), 77 deletions(-) create mode 100644 github-action/src/utils/bumpVersion.ts diff --git a/.github/workflows/manual-deploy-prod.yml b/.github/workflows/manual-deploy-prod.yml index 24ca0034b..bddc5d239 100644 --- a/.github/workflows/manual-deploy-prod.yml +++ b/.github/workflows/manual-deploy-prod.yml @@ -58,4 +58,4 @@ jobs: env: #Public hosted runners has 16GB available (linux | windows runners) NODE_OPTIONS: "--max_old_space_size=12288" - run: npx turbo run fprd:deploy --filter=${{inputs.appKey}} --concurrency 4 --token ${{ steps.get-fusion-token.outputs.token }} --ai '${{secrets.ai}}' --modelViewerConfig '${{vars.modelViewerConfig}}' --sha '${{github.sha}}' + run: npx turbo run fprd:deploy --filter='${{inputs.appKey}}' --concurrency 4 -- --token ${{ steps.get-fusion-token.outputs.token }} --ai '${{secrets.ai}}' --modelViewerConfig '${{vars.modelViewerConfig}}' --sha '${{github.sha}}' diff --git a/github-action/src/releaseMain.ts b/github-action/src/releaseMain.ts index 56b6d9490..ce280684f 100644 --- a/github-action/src/releaseMain.ts +++ b/github-action/src/releaseMain.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { HttpClient } from '@actions/http-client'; import { readdirSync } from 'fs'; import { Command } from 'commander'; import { setSecret, warning } from '@actions/core'; @@ -10,8 +11,9 @@ import { zipBundle } from './utils/zipBundle.js'; import { uploadBundle } from './utils/uploadBundle.js'; import { patchAppConfig } from './utils/patchAppConfig.js'; import { execSync } from 'child_process'; +import { getVersion } from './utils/bumpVersion.js'; -const prodUrl = 'https://fusion-s-portal-fprd.azurewebsites.net'; +const prodUrl = 'https://apps.api.fusion.equinor.com'; const program = new Command(); @@ -47,6 +49,7 @@ program await program.parseAsync(); + export async function release(config: ReleaseArgs) { const pkg = parsePackageJson(); if (!pkg.name) { @@ -62,11 +65,12 @@ export async function release(config: ReleaseArgs) { prepareBundle(); - makeManifest('./package.json'); + const version = await getVersion(prodUrl, config.token, pkg.name); + makeManifest('./package.json', version, config.sha); const zipped = zipBundle(); - await uploadBundle(prodUrl, config.token, pkg.name, zipped); + await uploadBundle(prodUrl, config.token, pkg.name, zipped, version); await patchAppConfig( { ai: config.ai, diff --git a/github-action/src/releasePr.ts b/github-action/src/releasePr.ts index 4d4241ab3..351007098 100644 --- a/github-action/src/releasePr.ts +++ b/github-action/src/releasePr.ts @@ -9,8 +9,9 @@ import { zipBundle } from './utils/zipBundle.js'; import { uploadBundle } from './utils/uploadBundle.js'; import { patchAppConfig } from './utils/patchAppConfig.js'; import { execSync } from 'child_process'; +import { getVersion } from './utils/bumpVersion.js'; -const ciUrl = 'https://fusion-s-portal-ci.azurewebsites.net'; +const ciUrl = 'https://apps.ci.api.fusion-dev.net'; const program = new Command(); @@ -49,15 +50,17 @@ await program.parseAsync(); export async function release(context: ReleaseArgs) { prepareBundle(); - makeManifest('./package.json'); - const zipped = zipBundle(); - const r = parsePackageJson(); - if (!r.name) { + const pkg = parsePackageJson(); + if (!pkg.name) { throw new Error( `No name in package json, cannot deploy unknown app at path ${process.cwd()}` ); } - await uploadBundle(ciUrl, context.token, r.name, zipped); + + const version = await getVersion(ciUrl, context.token, pkg.name); + makeManifest('./package.json', version, context.sha); + const zipped = zipBundle(); + await uploadBundle(ciUrl, context.token, pkg.name, zipped, version); await patchAppConfig( { ai: context.ai, @@ -66,9 +69,11 @@ export async function release(context: ReleaseArgs) { modelViewerConfig: JSON.parse(context.modelViewerConfig), }, context.token, - r.name, + pkg.name, ciUrl ); - execSync(`echo '## ${r.name}' >> $GITHUB_STEP_SUMMARY`); + execSync(`echo '## ${pkg.name}' >> $GITHUB_STEP_SUMMARY`); } + + diff --git a/github-action/src/utils/bumpVersion.ts b/github-action/src/utils/bumpVersion.ts new file mode 100644 index 000000000..1b26515cc --- /dev/null +++ b/github-action/src/utils/bumpVersion.ts @@ -0,0 +1,23 @@ +import { HttpClient } from '@actions/http-client'; + +// We do not use semver and dont want to manually bump versions. +// For now we ask the server what the latest version is and bump the patch version +export async function getVersion(ciUrl: string, token: string, name: string) { + const client = new HttpClient(); + const response = await client.get(`${ciUrl}/apps/${name}?api-version=1.0`, { + ['Authorization']: `Bearer ${token}`, + }); + const body = await response.readBody(); + const json = JSON.parse(body); + const v = incrementPatchVersion(json.build.version); + return v; +} + +function incrementPatchVersion(semver: string) { + const parts = semver.split('.'); + if (parts.length !== 3) { + throw new Error('Invalid semver format: ' + semver); + } + const patch = parseInt(parts[2], 10) + 1; + return `${parts[0]}.${parts[1]}.${patch}`; +} diff --git a/github-action/src/utils/makeManifest.ts b/github-action/src/utils/makeManifest.ts index ad9e683b2..05f2e7028 100644 --- a/github-action/src/utils/makeManifest.ts +++ b/github-action/src/utils/makeManifest.ts @@ -1,29 +1,20 @@ import { parsePackageJson } from './parsePackageJson.js'; import fs from 'fs'; -import { notice } from '@actions/core'; -export function makeManifest(path: string) { +export function makeManifest(path: string, version: string, sha: string) { // Create manifest - notice('making manifest'); - const { version, name, ...maybe } = parsePackageJson(path); + const { name } = parsePackageJson(path); if (!version || !name) { throw new Error('Name or version missing in package.json'); } - const { major, minor, patch } = splitVersions(version); - - /** Some app-manifests have custom short and displaynames */ - const shortName = maybe?.['shortName'] ?? name; - const displayName = maybe?.['displayName'] ?? name[0].toUpperCase() + name.slice(1); - const manifest = { - name: displayName, - shortName: shortName, - key: name, - version: { - major: major, - minor: minor, - patch: patch, - }, + //required + entryPoint: "app-bundle.js", + //required + version: version, + githubRepo: "https://github.com/equinor/cc-components", + timestamp: new Date().toISOString(), + commitSha: sha, }; const data = JSON.stringify(manifest, null, 2); @@ -31,11 +22,3 @@ export function makeManifest(path: string) { fs.writeFileSync('./dist/app-manifest.json', data); } -function splitVersions(version: string) { - const [major, minor, patch] = version.split('.'); - return { - major, - minor, - patch, - }; -} diff --git a/github-action/src/utils/patchAppConfig.ts b/github-action/src/utils/patchAppConfig.ts index c28d87089..b19405662 100644 --- a/github-action/src/utils/patchAppConfig.ts +++ b/github-action/src/utils/patchAppConfig.ts @@ -10,7 +10,7 @@ export async function getAppConfig(token: string, appKey: string, url: string) { ['Content-Type']: 'application/json', }; - const res = await client.get(`${url}/api/apps/${appKey}/config`, headers); + const res = await client.get(`${url}/apps/${appKey}/builds/latest/config`, headers); if (res.message.statusCode !== 200) { logInfo(`Failed to fetch client config, Code: ${res.message.statusCode}`, 'Red'); throw new Error('Failed to fetch client config'); @@ -49,7 +49,7 @@ export async function patchAppConfig = {} //patch const patchResponse = await client.put( - `${url}/api/apps/${appKey}/config`, + `${url}/apps/${appKey}/builds/latest/config`, JSON.stringify(existingConfig), headers ); diff --git a/github-action/src/utils/uploadBundle.ts b/github-action/src/utils/uploadBundle.ts index 40392a070..9ddbae557 100644 --- a/github-action/src/utils/uploadBundle.ts +++ b/github-action/src/utils/uploadBundle.ts @@ -10,12 +10,11 @@ export async function uploadBundle( baseUrl: string, token: string, appKey: string, - zipped: AdmZip + zipped: AdmZip, + version: string ) { const client = new HttpClient(); - await ensureAppExists(baseUrl, token, appKey); - const headers: OutgoingHttpHeaders = { ['Authorization']: `Bearer ${token}`, ['Content-Type']: 'application/zip', @@ -23,50 +22,36 @@ export async function uploadBundle( }; const stream = Readable.from(zipped.toBuffer()); - + logInfo(`Sending payload to ${baseUrl}/bundles/apps/${appKey}`, "Green"); const r = await client.sendStream( 'POST', - `${baseUrl}/api/apps/${appKey}/versions`, + `${baseUrl}/bundles/apps/${appKey}`, stream, headers ); - notice(`bundle uploaded with status code ${r.message.statusCode}`); - if (r.message.statusCode !== 200) { + notice(`${appKey} bundle uploaded with status code ${r.message.statusCode}`); + if (r.message.statusCode !== 201) { + const body = await r.readBody() logInfo(`Failed to upload ${appKey}, code: ${r.message.statusCode}`, 'Red'); + logInfo(body, 'Red'); throw new Error('Bundle failed to upload, fatal error'); } /** Publish bundle */ - const publishResponse = await client.post( - `${baseUrl}/api/apps/${appKey}/publish`, - '', - headers + const publishResponse = await client.put( + `${baseUrl}/apps/${appKey}/tags/latest`, + JSON.stringify({ version: version }), + { + ['Authorization']: `Bearer ${token}`, + ["Content-Type"]: 'application/json', + } ); + if (publishResponse.message.statusCode !== 200) { - logInfo(`Failed to publish ${appKey}, code: ${r.message.statusCode}`, 'Red'); - throw new Error(JSON.stringify(publishResponse.message)); + logInfo(`Failed to publish ${appKey}, code: ${publishResponse.message.statusCode}`, 'Red'); + const body = await publishResponse.readBody() + throw new Error(body) } logInfo(`Sucessfully published ${appKey}`, 'Green'); } - -async function ensureAppExists(baseUrl: string, token: string, appKey: string) { - const client = new HttpClient(); - - const headers: OutgoingHttpHeaders = { - ['Authorization']: `Bearer ${token}`, - ['Content-Type']: 'application/zip', - }; - - const res = await client.get( - `${baseUrl}/api/admin/apps/${appKey}/versions?api-version=1.0`, - headers - ); - - if (res.message.statusCode === 404) { - logInfo(`Unknown app: ${appKey}`, 'Red'); - throw new Error( - 'App doesnt exist please use the manual create fusion app to create this app first' - ); - } -} diff --git a/github-action/src/utils/zipBundle.ts b/github-action/src/utils/zipBundle.ts index 83e1bb472..fa64377eb 100644 --- a/github-action/src/utils/zipBundle.ts +++ b/github-action/src/utils/zipBundle.ts @@ -1,18 +1,17 @@ import { resolve } from 'path'; import AdmZip from 'adm-zip'; -import { notice } from '@actions/core'; export function zipBundle() { - // zip bundle - notice('zipping bundle'); const appManifestPath = resolve('./dist/app-manifest.json'); const bundlePath = resolve('./dist/app-bundle.js'); + const packageJsonPath = resolve('./package.json'); var zip = new AdmZip(); //TODO: scan files in package.json zip.addLocalFile(appManifestPath); zip.addLocalFile(bundlePath); + zip.addLocalFile(packageJsonPath); zip.writeZip('./dist/bundle.zip'); return zip;