diff --git a/.github/workflows/deploy-fern-bot-dev.yml b/.github/workflows/deploy-fern-bot-dev.yml index 448c68f30d..372e41b590 100644 --- a/.github/workflows/deploy-fern-bot-dev.yml +++ b/.github/workflows/deploy-fern-bot-dev.yml @@ -21,6 +21,8 @@ env: GITHUB_APP_WEBHOOK_SECRET: ${{ secrets.FERN_BOT_DEV_GITHUB_APP_WEBHOOK_SECRET }} DEFAULT_VENUS_ORIGIN: "https://venus-dev2.buildwithfern.com" DEFAULT_FDR_ORIGIN: "https://registry-dev2.buildwithfern.com" + FERNIE_SLACK_APP_TOKEN: ${{ secrets.FERNIE_SLACK_APP_TOKEN }} + CUSTOMER_ALERTS_SLACK_CHANNEL: "customer-upgrades-dev" CO_API_KEY: ${{ secrets.DEV_CO_API_KEY }} jobs: @@ -28,12 +30,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - + - name: 📥 Install uses: ./.github/actions/install - name: Compile - run: pnpm compile + run: pnpm compile - name: 🚀 serverless deploy env: diff --git a/.github/workflows/deploy-fern-bot-prod.yml b/.github/workflows/deploy-fern-bot-prod.yml index ac7104411f..db320e1e89 100644 --- a/.github/workflows/deploy-fern-bot-prod.yml +++ b/.github/workflows/deploy-fern-bot-prod.yml @@ -15,6 +15,8 @@ env: GITHUB_APP_CLIENT_ID: ${{ secrets.FERN_BOT_PROD_GITHUB_APP_CLIENT_ID }} GITHUB_APP_CLIENT_SECRET: ${{ secrets.FERN_BOT_PROD_GITHUB_APP_CLIENT_SECRET }} GITHUB_APP_WEBHOOK_SECRET: ${{ secrets.FERN_BOT_PROD_GITHUB_APP_WEBHOOK_SECRET }} + FERNIE_SLACK_APP_TOKEN: ${{ secrets.FERNIE_SLACK_APP_TOKEN }} + CUSTOMER_ALERTS_SLACK_CHANNEL: "customer-upgrades" DEFAULT_VENUS_ORIGIN: "https://venus.buildwithfern.com" DEFAULT_FDR_ORIGIN: "https://registry.buildwithfern.com" CO_API_KEY: ${{ secrets.PROD_CO_API_KEY }} @@ -25,13 +27,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - + - name: 📥 Install uses: ./.github/actions/install - name: Compile - run: pnpm compile - + run: pnpm compile + - name: 🚀 serverless deploy env: CI: false diff --git a/.github/workflows/test-fern-bot.yml b/.github/workflows/test-fern-bot.yml index 8d3f52143d..bb11fb2975 100644 --- a/.github/workflows/test-fern-bot.yml +++ b/.github/workflows/test-fern-bot.yml @@ -21,6 +21,8 @@ env: GITHUB_APP_WEBHOOK_SECRET: ${{ secrets.FERN_BOT_DEV_GITHUB_APP_WEBHOOK_SECRET }} DEFAULT_VENUS_ORIGIN: "https://venus-dev2.buildwithfern.com" DEFAULT_FDR_ORIGIN: "https://registry-dev2.buildwithfern.com" + FERNIE_SLACK_APP_TOKEN: ${{ secrets.FERNIE_SLACK_APP_TOKEN }} + CUSTOMER_ALERTS_SLACK_CHANNEL: "customer-upgrades-dev" CO_API_KEY: ${{ secrets.DEV_CO_API_KEY }} jobs: diff --git a/packages/commons/github/src/createOrUpdatePullRequest.ts b/packages/commons/github/src/createOrUpdatePullRequest.ts index a48fda5601..be210aba0b 100644 --- a/packages/commons/github/src/createOrUpdatePullRequest.ts +++ b/packages/commons/github/src/createOrUpdatePullRequest.ts @@ -37,7 +37,7 @@ export async function createOrUpdatePullRequest( baseRepository: string, headRepository: string, branchName: string, -): Promise { +): Promise { const [headOwner] = headRepository.split("/"); const headBranch = `${headOwner}:${branchName}`; @@ -60,6 +60,8 @@ export async function createOrUpdatePullRequest( created: true, })}`, ); + + return pull.html_url; } catch (e) { if (getErrorMessage(e).includes("A pull request already exists for")) { console.error(`A pull request already exists for ${headBranch}`); @@ -96,4 +98,6 @@ export async function createOrUpdatePullRequest( created: false, })}`, ); + + return pull.html_url; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa02f89162..a2618a20d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2492,6 +2492,9 @@ importers: '@octokit/openapi-types': specifier: ^22.1.0 version: 22.2.0 + '@slack/web-api': + specifier: ^6.9.0 + version: 6.12.0 cohere-ai: specifier: ^7.9.5 version: 7.9.5 @@ -11541,6 +11544,11 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + json5@2.2.2: + resolution: {integrity: sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==} + engines: {node: '>=6'} + hasBin: true + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -16508,7 +16516,7 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.572.0 + '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) '@aws-sdk/client-sts': 3.572.0 '@aws-sdk/core': 3.572.0 '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0))(@aws-sdk/client-sts@3.572.0) @@ -16557,10 +16565,10 @@ snapshots: '@aws-crypto/sha1-browser': 3.0.0 '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.572.0 - '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) + '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) + '@aws-sdk/client-sts': 3.572.0 '@aws-sdk/core': 3.572.0 - '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)) + '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0) '@aws-sdk/middleware-bucket-endpoint': 3.568.0 '@aws-sdk/middleware-expect-continue': 3.572.0 '@aws-sdk/middleware-flexible-checksums': 3.572.0 @@ -16615,51 +16623,6 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.572.0': - dependencies: - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.572.0 - '@aws-sdk/core': 3.572.0 - '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)) - '@aws-sdk/middleware-host-header': 3.567.0 - '@aws-sdk/middleware-logger': 3.568.0 - '@aws-sdk/middleware-recursion-detection': 3.567.0 - '@aws-sdk/middleware-user-agent': 3.572.0 - '@aws-sdk/region-config-resolver': 3.572.0 - '@aws-sdk/types': 3.567.0 - '@aws-sdk/util-endpoints': 3.572.0 - '@aws-sdk/util-user-agent-browser': 3.567.0 - '@aws-sdk/util-user-agent-node': 3.568.0 - '@smithy/config-resolver': 2.2.0 - '@smithy/core': 1.4.2 - '@smithy/fetch-http-handler': 2.5.0 - '@smithy/hash-node': 2.2.0 - '@smithy/invalid-dependency': 2.2.0 - '@smithy/middleware-content-length': 2.2.0 - '@smithy/middleware-endpoint': 2.5.1 - '@smithy/middleware-retry': 2.3.1 - '@smithy/middleware-serde': 2.3.0 - '@smithy/middleware-stack': 2.2.0 - '@smithy/node-config-provider': 2.3.0 - '@smithy/node-http-handler': 2.5.0 - '@smithy/protocol-http': 3.3.0 - '@smithy/smithy-client': 2.5.1 - '@smithy/types': 2.12.0 - '@smithy/url-parser': 2.2.0 - '@smithy/util-base64': 2.3.0 - '@smithy/util-body-length-browser': 2.2.0 - '@smithy/util-body-length-node': 2.3.0 - '@smithy/util-defaults-mode-browser': 2.2.1 - '@smithy/util-defaults-mode-node': 2.3.1 - '@smithy/util-endpoints': 1.2.0 - '@smithy/util-middleware': 2.2.0 - '@smithy/util-retry': 2.2.0 - '@smithy/util-utf8': 2.3.0 - tslib: 2.6.2 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 @@ -16753,54 +16716,9 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.572.0 - '@aws-sdk/core': 3.572.0 - '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0) - '@aws-sdk/middleware-host-header': 3.567.0 - '@aws-sdk/middleware-logger': 3.568.0 - '@aws-sdk/middleware-recursion-detection': 3.567.0 - '@aws-sdk/middleware-user-agent': 3.572.0 - '@aws-sdk/region-config-resolver': 3.572.0 - '@aws-sdk/types': 3.567.0 - '@aws-sdk/util-endpoints': 3.572.0 - '@aws-sdk/util-user-agent-browser': 3.567.0 - '@aws-sdk/util-user-agent-node': 3.568.0 - '@smithy/config-resolver': 2.2.0 - '@smithy/core': 1.4.2 - '@smithy/fetch-http-handler': 2.5.0 - '@smithy/hash-node': 2.2.0 - '@smithy/invalid-dependency': 2.2.0 - '@smithy/middleware-content-length': 2.2.0 - '@smithy/middleware-endpoint': 2.5.1 - '@smithy/middleware-retry': 2.3.1 - '@smithy/middleware-serde': 2.3.0 - '@smithy/middleware-stack': 2.2.0 - '@smithy/node-config-provider': 2.3.0 - '@smithy/node-http-handler': 2.5.0 - '@smithy/protocol-http': 3.3.0 - '@smithy/smithy-client': 2.5.1 - '@smithy/types': 2.12.0 - '@smithy/url-parser': 2.2.0 - '@smithy/util-base64': 2.3.0 - '@smithy/util-body-length-browser': 2.2.0 - '@smithy/util-body-length-node': 2.3.0 - '@smithy/util-defaults-mode-browser': 2.2.1 - '@smithy/util-defaults-mode-node': 2.3.1 - '@smithy/util-endpoints': 1.2.0 - '@smithy/util-middleware': 2.2.0 - '@smithy/util-retry': 2.2.0 - '@smithy/util-utf8': 2.3.0 - tslib: 2.6.2 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/client-sts@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)': - dependencies: - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.572.0 + '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) '@aws-sdk/core': 3.572.0 - '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)) + '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0))(@aws-sdk/client-sts@3.572.0) '@aws-sdk/middleware-host-header': 3.567.0 '@aws-sdk/middleware-logger': 3.568.0 '@aws-sdk/middleware-recursion-detection': 3.567.0 @@ -16837,7 +16755,6 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.6.2 transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/core@3.572.0': @@ -16874,23 +16791,6 @@ snapshots: '@aws-sdk/client-sts': 3.572.0 '@aws-sdk/credential-provider-env': 3.568.0 '@aws-sdk/credential-provider-process': 3.572.0 - '@aws-sdk/credential-provider-sso': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0)) - '@aws-sdk/credential-provider-web-identity': 3.568.0(@aws-sdk/client-sts@3.572.0) - '@aws-sdk/types': 3.567.0 - '@smithy/credential-provider-imds': 2.3.0 - '@smithy/property-provider': 2.2.0 - '@smithy/shared-ini-file-loader': 2.4.0 - '@smithy/types': 2.12.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - aws-crt - - '@aws-sdk/credential-provider-ini@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0(@aws-sdk/client-sso-oidc@3.572.0))': - dependencies: - '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) - '@aws-sdk/credential-provider-env': 3.568.0 - '@aws-sdk/credential-provider-process': 3.572.0 '@aws-sdk/credential-provider-sso': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) '@aws-sdk/credential-provider-web-identity': 3.568.0(@aws-sdk/client-sts@3.572.0) '@aws-sdk/types': 3.567.0 @@ -16926,25 +16826,6 @@ snapshots: '@aws-sdk/credential-provider-http': 3.568.0 '@aws-sdk/credential-provider-ini': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0))(@aws-sdk/client-sts@3.572.0) '@aws-sdk/credential-provider-process': 3.572.0 - '@aws-sdk/credential-provider-sso': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0)) - '@aws-sdk/credential-provider-web-identity': 3.568.0(@aws-sdk/client-sts@3.572.0) - '@aws-sdk/types': 3.567.0 - '@smithy/credential-provider-imds': 2.3.0 - '@smithy/property-provider': 2.2.0 - '@smithy/shared-ini-file-loader': 2.4.0 - '@smithy/types': 2.12.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - '@aws-sdk/client-sts' - - aws-crt - - '@aws-sdk/credential-provider-node@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0(@aws-sdk/client-sso-oidc@3.572.0))': - dependencies: - '@aws-sdk/credential-provider-env': 3.568.0 - '@aws-sdk/credential-provider-http': 3.568.0 - '@aws-sdk/credential-provider-ini': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)) - '@aws-sdk/credential-provider-process': 3.572.0 '@aws-sdk/credential-provider-sso': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) '@aws-sdk/credential-provider-web-identity': 3.568.0(@aws-sdk/client-sts@3.572.0) '@aws-sdk/types': 3.567.0 @@ -16985,19 +16866,6 @@ snapshots: '@smithy/types': 2.12.0 tslib: 2.6.2 - '@aws-sdk/credential-provider-sso@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0))': - dependencies: - '@aws-sdk/client-sso': 3.572.0 - '@aws-sdk/token-providers': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0)) - '@aws-sdk/types': 3.567.0 - '@smithy/property-provider': 2.2.0 - '@smithy/shared-ini-file-loader': 2.4.0 - '@smithy/types': 2.12.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - aws-crt - '@aws-sdk/credential-provider-sso@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)': dependencies: '@aws-sdk/client-sso': 3.572.0 @@ -17013,7 +16881,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.568.0(@aws-sdk/client-sts@3.572.0)': dependencies: - '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) + '@aws-sdk/client-sts': 3.572.0 '@aws-sdk/types': 3.567.0 '@smithy/property-provider': 2.2.0 '@smithy/types': 2.12.0 @@ -17138,18 +17006,9 @@ snapshots: '@smithy/types': 2.12.0 tslib: 2.6.2 - '@aws-sdk/token-providers@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0))': - dependencies: - '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) - '@aws-sdk/types': 3.567.0 - '@smithy/property-provider': 2.2.0 - '@smithy/shared-ini-file-loader': 2.4.0 - '@smithy/types': 2.12.0 - tslib: 2.6.2 - '@aws-sdk/token-providers@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.572.0 + '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) '@aws-sdk/types': 3.567.0 '@smithy/property-provider': 2.2.0 '@smithy/shared-ini-file-loader': 2.4.0 @@ -26450,7 +26309,7 @@ snapshots: ignore: 5.3.1 is-core-module: 2.13.1 js-yaml: 3.14.1 - json5: 2.2.3 + json5: 2.2.2 lodash: 4.17.21 minimatch: 7.4.6 multimatch: 5.0.0 @@ -29342,7 +29201,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 18.19.33 + '@types/node': 20.12.12 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -29527,6 +29386,8 @@ snapshots: dependencies: minimist: 1.2.8 + json5@2.2.2: {} + json5@2.2.3: {} jsonc-parser@2.2.1: {} diff --git a/servers/fern-bot/.env b/servers/fern-bot/.env index a6ea96cac3..b2e6e69ce2 100644 --- a/servers/fern-bot/.env +++ b/servers/fern-bot/.env @@ -10,4 +10,5 @@ GITHUB_APP_CLIENT_SECRET="FILL ME IN" GITHUB_APP_WEBHOOK_SECRET="FILL ME IN" CO_API_KEY="FILL ME IN" DEFAULT_VENUS_ORIGIN="FILL ME IN" -DEFAULT_FDR_ORIGIN="FILL ME IN" \ No newline at end of file +DEFAULT_FDR_ORIGIN="FILL ME IN" +FERNIE_SLACK_APP_TOKEN="FILL ME IN" \ No newline at end of file diff --git a/servers/fern-bot/package.json b/servers/fern-bot/package.json index 0d3cbbc71c..7af5c3f0f2 100644 --- a/servers/fern-bot/package.json +++ b/servers/fern-bot/package.json @@ -24,6 +24,7 @@ "@fern-api/venus-api-sdk": "0.8.1-1-gd6d1a5b", "@fern-fern/generators-sdk": "0.107.0-892e7c392", "@octokit/openapi-types": "^22.1.0", + "@slack/web-api": "^6.9.0", "cohere-ai": "^7.9.5", "execa": "^5.1.1", "fern-api": "^0.21.0", diff --git a/servers/fern-bot/serverless.yml b/servers/fern-bot/serverless.yml index 224004ccd8..f6f1b8dcba 100644 --- a/servers/fern-bot/serverless.yml +++ b/servers/fern-bot/serverless.yml @@ -24,6 +24,9 @@ provider: GITHUB_APP_WEBHOOK_SECRET: ${env:GITHUB_APP_WEBHOOK_SECRET, 'placeholder'} CO_API_KEY: ${env:CO_API_KEY, 'placeholder'} DEFAULT_VENUS_ORIGIN: ${env:DEFAULT_VENUS_ORIGIN, 'placeholder'} + DEFAULT_FDR_ORIGIN: ${env:DEFAULT_FDR_ORIGIN, 'placeholder'} + FERNIE_SLACK_APP_TOKEN: ${env:FERNIE_SLACK_APP_TOKEN, 'placeholder'} + CUSTOMER_ALERTS_SLACK_CHANNEL: ${env:CUSTOMER_ALERTS_SLACK_CHANNEL, 'placeholder'} REPO_TO_RUN_ON: ${env:REPO_TO_RUN_ON, 'OMIT'} # Roles for the lambda functions iam: diff --git a/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersion.ts b/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersion.ts index 5573ac6b9f..06e36cc432 100644 --- a/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersion.ts +++ b/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersion.ts @@ -17,6 +17,8 @@ export async function updateGeneratorVersionInternal(env: Env, repoData: RepoDat env.GITHUB_APP_LOGIN_ID, env.DEFAULT_VENUS_ORIGIN, env.DEFAULT_FDR_ORIGIN, + env.FERNIE_SLACK_APP_TOKEN, + env.CUSTOMER_ALERTS_SLACK_CHANNEL, ); } }); diff --git a/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersions.ts b/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersions.ts index 55d0dd2993..60b438add8 100644 --- a/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersions.ts +++ b/servers/fern-bot/src/functions/generator-updates/actions/updateGeneratorVersions.ts @@ -23,6 +23,8 @@ export async function updateGeneratorVersionsInternal(env: Env): Promise { env.GITHUB_APP_LOGIN_ID, env.DEFAULT_VENUS_ORIGIN, env.DEFAULT_FDR_ORIGIN, + env.FERNIE_SLACK_APP_TOKEN, + env.CUSTOMER_ALERTS_SLACK_CHANNEL, ); }); } diff --git a/servers/fern-bot/src/functions/generator-updates/shared/updateGeneratorInternal.ts b/servers/fern-bot/src/functions/generator-updates/shared/updateGeneratorInternal.ts index 79a4c79c94..8ca65f5337 100644 --- a/servers/fern-bot/src/functions/generator-updates/shared/updateGeneratorInternal.ts +++ b/servers/fern-bot/src/functions/generator-updates/shared/updateGeneratorInternal.ts @@ -4,13 +4,12 @@ import { FernRegistryClient } from "@fern-fern/generators-sdk"; import { ChangelogResponse } from "@fern-fern/generators-sdk/api/resources/generators"; import { execFernCli } from "@libs/fern"; import { DEFAULT_REMOTE_NAME, cloneRepo, configureGit, type Repository } from "@libs/github/utilities"; +import { GeneratorMessageMetadata, SlackService } from "@libs/slack/SlackService"; import yaml from "js-yaml"; import { Octokit } from "octokit"; import { SimpleGit } from "simple-git"; -async function isOrganizationCanary(venusUrl: string, fullRepoPath: string): Promise { - const orgId = cleanStdout((await execFernCli("organization", fullRepoPath)).stdout); - console.log(`Found organization ID: ${orgId}`); +async function isOrganizationCanary(orgId: string, venusUrl: string): Promise { const client = new FernVenusApiClient({ environment: venusUrl }); const response = await client.organization.get(FernVenusApi.OrganizationId(orgId)); @@ -132,13 +131,18 @@ export async function updateVersionInternal( fernBotLoginId: string, venusUrl: string, fdrUrl: string, + slackToken: string, + slackChannel: string, ): Promise { const [git, fullRepoPath] = await configureGit(repository); console.log(`Cloning repo: ${repository.clone_url} to ${fullRepoPath}`); await cloneRepo(git, repository, octokit, fernBotLoginName, fernBotLoginId); + const organization = cleanStdout((await execFernCli("organization", fullRepoPath)).stdout); + console.log(`Found organization ID: ${organization}`); + try { - if (!(await isOrganizationCanary(venusUrl, fullRepoPath))) { + if (!(await isOrganizationCanary(organization, venusUrl))) { console.log("Organization is not a fern-bot canary, skipping upgrade."); return; } @@ -147,6 +151,8 @@ export async function updateVersionInternal( throw error; } + const slackClient = new SlackService(slackToken, slackChannel); + await handleSingleUpgrade({ octokit, repository, @@ -165,6 +171,8 @@ export async function updateVersionInternal( getEntityVersion: async () => { return cleanStdout((await execFernCli("--version", fullRepoPath)).stdout); }, + slackClient, + organization, }); // Pull a branch of fern/update//: @@ -189,11 +197,14 @@ export async function updateVersionInternal( git, branchName: `${branchName}${additionalName}`, prTitle: `Upgrade Fern Generator Version: (${additionalName})`, - upgradeAction: async () => { + upgradeAction: async ({ includeMajor }: { includeMajor?: boolean }) => { let command = `generator upgrade --generator ${generatorName} --group ${groupName}`; if (apiName !== NO_API_FALLBACK_KEY) { command += ` --api ${apiName}`; } + if (includeMajor) { + command += " --include-major"; + } const response = await execFernCli(command, fullRepoPath); console.log(response.stdout); console.log(response.stderr); @@ -210,6 +221,15 @@ export async function updateVersionInternal( } return cleanStdout((await execFernCli(command, fullRepoPath)).stdout); }, + maybeGetGeneratorMetadata: async () => { + return { + group: groupName, + generatorName, + apiName: apiName !== NO_API_FALLBACK_KEY ? apiName : undefined, + }; + }, + slackClient, + organization, }); } } @@ -225,15 +245,21 @@ async function handleSingleUpgrade({ upgradeAction, getPRBody, getEntityVersion, + maybeGetGeneratorMetadata, + slackClient, + organization, }: { octokit: Octokit; repository: Repository; git: SimpleGit; branchName: string; prTitle: string; - upgradeAction: () => Promise; + upgradeAction: ({ includeMajor }: { includeMajor?: boolean }) => Promise; getPRBody: (fromVersion: string, toversion: string) => Promise; getEntityVersion: () => Promise; + maybeGetGeneratorMetadata?: () => Promise; + slackClient: SlackService; + organization: string; }): Promise { // Before we checkout a new branch, we need to ensure we have the current version off the default branch // Checkout the default branch and run the version command to get the current version @@ -246,7 +272,7 @@ async function handleSingleUpgrade({ // Perform the upgrade and get the new version you just upgraded to console.log(`Upgrading entity to latest version, from version: ${fromVersion}`); - await upgradeAction(); + await upgradeAction({}); const toVersion = await getEntityVersion(); console.log(`Upgraded entity to latest version, to version: ${toVersion}`); @@ -262,7 +288,7 @@ async function handleSingleUpgrade({ await git.push(["--force-with-lease", DEFAULT_REMOTE_NAME, `${branchName}:refs/heads/${branchName}`]); // Open a PR, or update it in place - await createOrUpdatePullRequest( + const prUrl = await createOrUpdatePullRequest( octokit, { title: `:herb: :sparkles: [Scheduled] ${prTitle}`, @@ -273,7 +299,31 @@ async function handleSingleUpgrade({ repository.full_name, branchName, ); - } else { - console.log("No changes detected, skipping PR creation"); + + // Notify via slack that the upgrade PR was created + await slackClient.notifyUpgradePRCreated({ + fromVersion, + toVersion, + prUrl, + repoName: repository.full_name, + generator: maybeGetGeneratorMetadata ? await maybeGetGeneratorMetadata() : undefined, + organization, + }); + return; + } else if (fromVersion === toVersion) { + await upgradeAction({ includeMajor: true }); + const toVersion = await getEntityVersion(); + if (fromVersion !== toVersion) { + slackClient.notifyMajorVersionUpgradeEncountered({ + repoUrl: repository.html_url, + repoName: repository.full_name, + currentVersion: fromVersion, + organization, + }); + console.log("No change made as the upgrade is across major versions."); + return; + } } + + console.log("No changes detected, skipping PR creation"); } diff --git a/servers/fern-bot/src/libs/env.ts b/servers/fern-bot/src/libs/env.ts index 3eac92d7ac..ac0ecf2500 100644 --- a/servers/fern-bot/src/libs/env.ts +++ b/servers/fern-bot/src/libs/env.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain */ // This is a placeholder for envvars that are unset, since serverless doesn't support passing undefined export const OMIT = "OMIT"; @@ -14,6 +15,8 @@ export interface Env { REPO_DATA_S3_KEY?: string; DEFAULT_VENUS_ORIGIN: string; DEFAULT_FDR_ORIGIN: string; + CUSTOMER_ALERTS_SLACK_CHANNEL: string; + FERNIE_SLACK_APP_TOKEN: string; } export function evaluateEnv(): Env { @@ -23,26 +26,19 @@ export function evaluateEnv(): Env { // These assertions are technically unsafe, but we don't want the bot to deploy without them return { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain GITHUB_APP_LOGIN_NAME: process?.env.GITHUB_APP_LOGIN_NAME!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain GITHUB_APP_LOGIN_ID: process?.env.GITHUB_APP_LOGIN_ID!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain GITHUB_APP_ID: process?.env.GITHUB_APP_ID!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain GITHUB_APP_PRIVATE_KEY: process?.env.GITHUB_APP_PRIVATE_KEY?.replaceAll("\\n", "\n")!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain GITHUB_APP_CLIENT_ID: process?.env.GITHUB_APP_CLIENT_ID!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain GITHUB_APP_CLIENT_SECRET: process?.env.GITHUB_APP_CLIENT_SECRET!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain GITHUB_APP_WEBHOOK_SECRET: process?.env.GITHUB_APP_WEBHOOK_SECRET!, REPO_TO_RUN_ON: repoToRunOn == null || repoToRunOn == OMIT ? undefined : repoToRunOn, REPO_DATA_S3_BUCKET: repoDataS3Bucket == null || repoDataS3Bucket == OMIT ? undefined : repoDataS3Bucket, REPO_DATA_S3_KEY: repoDataS3Key == null || repoDataS3Key == OMIT ? undefined : repoDataS3Key, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain DEFAULT_VENUS_ORIGIN: process?.env.DEFAULT_VENUS_ORIGIN!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain DEFAULT_FDR_ORIGIN: process?.env.DEFAULT_FDR_ORIGIN!, + FERNIE_SLACK_APP_TOKEN: process?.env.FERNIE_SLACK_APP_TOKEN!, + CUSTOMER_ALERTS_SLACK_CHANNEL: process?.env.CUSTOMER_ALERTS_SLACK_CHANNEL!, }; } diff --git a/servers/fern-bot/src/libs/slack/SlackService.ts b/servers/fern-bot/src/libs/slack/SlackService.ts new file mode 100644 index 0000000000..456f4f5faf --- /dev/null +++ b/servers/fern-bot/src/libs/slack/SlackService.ts @@ -0,0 +1,149 @@ +import { WebClient } from "@slack/web-api"; + +export interface GeneratorMessageMetadata { + group: string; + generatorName: string; + apiName?: string; +} + +export class SlackService { + private slackClient: WebClient; + + constructor( + slackToken: string, + private readonly slackChannel: string, + ) { + this.slackClient = new WebClient(slackToken); + } + + private getGeneratorMetadataMessage(generator: GeneratorMessageMetadata): string { + return `*Generator Group:* ${generator.group}\n*Generator:* ${generator.generatorName}${generator.apiName ? `\n*API:* ${generator.apiName}` : ""}`; + } + + // TODO: would be nice if we stored customer metadata in a DB to then add that information to this message + private addContextToHeader(header: string, organization: string, generatorName?: string): string { + if (generatorName == null) { + return `:fern: ${organization} - ${header} for Fern CLI`; + } + + if (generatorName.includes("python")) { + return `:python: ${organization} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("typescript")) { + return `:ts: ${organization} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("java")) { + return `:java: ${organization} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("ruby")) { + return `:ruby: ${organization} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("csharp")) { + return `:csharp: ${organization} - ${header} for \`${generatorName}\``; + } else if (generatorName.includes("go")) { + return `:gopher: ${organization} - ${header} for \`${generatorName}\``; + } + + return `:interrobang: ${header} for \`${generatorName}\``; + } + + public async notifyUpgradePRCreated({ + fromVersion, + toVersion, + prUrl, + repoName, + generator, + organization, + }: { + fromVersion: string; + toVersion: string; + prUrl: string; + repoName: string; + generator?: GeneratorMessageMetadata; + organization: string; + }): Promise { + await this.slackClient.chat.postMessage({ + channel: this.slackChannel, + blocks: [ + { + type: "header", + text: { + type: "plain_text", + text: this.addContextToHeader("Upgrade PR Created", organization, generator?.generatorName), + }, + }, + { + type: "divider", + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Organization*: ${organization}\n*Github Repo:* ${repoName}${generator ? "\n" + this.getGeneratorMetadataMessage(generator) : ""}\n*Upgrading:* ${fromVersion} :arrow_right: ${toVersion}`, + }, + }, + { + type: "actions", + block_id: "actions1", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "View PR :link:", + }, + url: prUrl, + }, + ], + }, + ], + }); + } + + public async notifyMajorVersionUpgradeEncountered({ + repoUrl, + repoName, + currentVersion, + generator, + organization, + }: { + repoUrl: string; + repoName: string; + currentVersion: string; + generator?: GeneratorMessageMetadata; + organization: string; + }): Promise { + await this.slackClient.chat.postMessage({ + channel: this.slackChannel, + blocks: [ + { + type: "header", + text: { + type: "plain_text", + text: `:rotating_light: ${this.addContextToHeader("Major version upgrade encountered", organization, generator?.generatorName)}`, + }, + }, + { + type: "divider", + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `Hey , we've encountered a major version upgrade which needs manual intervention!\n\n*Organization*: ${organization}\n*Github Repo*: ${repoName}${generator && "\n" + this.getGeneratorMetadataMessage(generator)}\n*Current version*: ${currentVersion}`, + }, + }, + { + type: "actions", + block_id: "actions1", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "Visit Repo :link:", + }, + url: repoUrl, + }, + ], + }, + ], + }); + } +}