Skip to content
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

feat: fern-bot now runs fern generator upgrade and fern upgrade on a cron (once a week) #825

Closed
wants to merge 9 commits into from
Closed
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
61 changes: 61 additions & 0 deletions .github/workflows/invoke-fern-bot-upgrade-generators.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Invoke FernBot - updateGeneratorVersions

on:
workflow_dispatch:
inputs:
environment:
type: choice
description: Which environment to run the workflow in
options:
- production
- development
repo:
description: "The repo to run the action against (of the form `owner/repo_name`), if omitted runs on all repos the app is installed on"
type: string

jobs:
invoke_dev:
if: ${{ github.event.inputs.environment == 'development' }}
runs-on: ubuntu-latest
env:
SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }}
GITHUB_APP_LOGIN_NAME: ${{ secrets.FERN_BOT_DEV_GITHUB_APP_LOGIN_NAME }}
GITHUB_APP_LOGIN_ID: ${{ secrets.FERN_BOT_DEV_GITHUB_APP_LOGIN_ID }}
GITHUB_APP_ID: ${{ secrets.FERN_BOT_DEV_GITHUB_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.FERN_BOT_DEV_GITHUB_APP_PRIVATE_KEY }}
GITHUB_APP_CLIENT_ID: ${{ secrets.FERN_BOT_DEV_GITHUB_APP_CLIENT_ID }}
GITHUB_APP_CLIENT_SECRET: ${{ secrets.FERN_BOT_DEV_GITHUB_APP_CLIENT_SECRET }}
GITHUB_APP_WEBHOOK_SECRET: ${{ secrets.FERN_BOT_DEV_GITHUB_APP_WEBHOOK_SECRET }}
CO_API_KEY: ${{ secrets.DEV_CO_API_KEY }}
REPO_TO_RUN_ON: ${{ github.event.inputs.repo }}
steps:
- uses: actions/checkout@v4
- name: 📥 Install
uses: ./.github/actions/install
- name: 🚀 serverless deploy
run: |
pnpm --filter "@fern-platform/fern-bot" install
pnpm --filter "@fern-platform/fern-bot" invoke local --function updateGeneratorVersions --stage development

invoke_prod:
if: ${{ github.event.inputs.environment == 'production' }}
runs-on: ubuntu-latest
env:
SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }}
GITHUB_APP_LOGIN_NAME: ${{ secrets.FERN_BOT_PROD_GITHUB_APP_LOGIN_NAME }}
GITHUB_APP_LOGIN_ID: ${{ secrets.FERN_BOT_PROD_GITHUB_APP_LOGIN_ID }}
GITHUB_APP_ID: ${{ secrets.FERN_BOT_PROD_GITHUB_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.FERN_BOT_PROD_GITHUB_APP_PRIVATE_KEY }}
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 }}
CO_API_KEY: ${{ secrets.PROD_CO_API_KEY }}
REPO_TO_RUN_ON: ${{ github.event.inputs.repo }}
steps:
- uses: actions/checkout@v4
- name: 📥 Install
uses: ./.github/actions/install
- name: 🚀 serverless deploy
run: |
pnpm --filter "@fern-platform/fern-bot" install
pnpm --filter "@fern-platform/fern-bot" invoke local --function updateGeneratorVersions --stage production
9 changes: 9 additions & 0 deletions servers/fern-bot/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ provider:
GITHUB_APP_CLIENT_SECRET: ${env:GITHUB_APP_CLIENT_SECRET, 'placeholder'}
GITHUB_APP_WEBHOOK_SECRET: ${env:GITHUB_APP_WEBHOOK_SECRET, 'placeholder'}
CO_API_KEY: ${env:CO_API_KEY, 'placeholder'}
VENUS_URL: ${env:VENUS_URL, 'placeholder'}
REPO_TO_RUN_ON: ${env:REPO_TO_RUN_ON, 'OMIT'}
functions:
updateOpenApiSpec:
Expand All @@ -29,6 +30,14 @@ functions:
- schedule:
rate: cron(0 0 * * ? *)
enabled: true
updateGeneratorVersions:
handler: "src/functions/generator-updates/updateGeneratorVersions.handler"
layers:
- arn:aws:lambda:us-east-1:553035198032:layer:git-lambda2:8
events:
- schedule:
rate: cron(0 1 * * MON *)
enabled: true
custom:
esbuild:
minify: true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Env } from "@libs/env";
import { execFernCli } from "@libs/fern";
import { setupGithubApp } from "@libs/github/octokit";
import {
DEFAULT_REMOTE_NAME,
cloneRepo,
configureGit,
createOrUpdatePullRequest,
getOrUpdateBranch,
type Repository,
} from "@libs/github/utilities";
import { App, Octokit } from "octokit";

const GENERATOR_UPDATE_BRANCH = "fern/update-generators";

async function updateGeneratorVersionInternal(
octokit: Octokit,
repository: Repository,
fernBotLoginName: string,
fernBotLoginId: string,
): Promise<void> {
const [git, fullRepoPath] = await configureGit(repository);
console.log(`Cloning repo: ${repository.clone_url} to ${fullRepoPath}`);
await cloneRepo(git, repository, octokit, fernBotLoginName, fernBotLoginId);

const originDefaultBranch = `${DEFAULT_REMOTE_NAME}/${repository.default_branch}`;
await getOrUpdateBranch(git, originDefaultBranch, GENERATOR_UPDATE_BRANCH);

try {
// Run API update command which will pull the new spec from the specified
// origin and write it to disk we can then commit it to github from there.
await execFernCli("upgrade", fullRepoPath);
await execFernCli("generator upgrade", fullRepoPath);
} catch (error) {
return;
}

console.log("Checking for changes to commit and push");
if (!(await git.status()).isClean()) {
console.log("Changes detected, committing and pushing");
// Add + commit files
await git.add(["-A"]);
await git.commit("(chore): upgrade generator versions to latest");

// Push the changes
await git.push([
"--force-with-lease",
DEFAULT_REMOTE_NAME,
`${GENERATOR_UPDATE_BRANCH}:refs/heads/${GENERATOR_UPDATE_BRANCH}`,
]);

// Open a PR
await createOrUpdatePullRequest(
octokit,
{
title: ":herb: :sparkles: [Scheduled] Upgrade SDK Generator Versions",
base: "main",
// TODO: This should really pull from the changelogs the generators maintain in the Fern repo
// at the least the CLI should output the versions of the generators it's upgrading, so we can display that
body: `## Automated Upgrade PR

<br/>

---

This Pull Request has been auto-generated as part of Fern's release process.`,
},
repository.full_name,
repository.full_name,
GENERATOR_UPDATE_BRANCH,
);
}
}

export async function updateGeneratorVersionsInternal(env: Env): Promise<void> {
const app: App = setupGithubApp(env);

if (env.REPO_TO_RUN_ON !== undefined) {
console.log("REPO_TO_RUN_ON has been specified, only running on:", env.REPO_TO_RUN_ON);
}
await app.eachRepository(async (installation) => {
if (env.REPO_TO_RUN_ON !== undefined && installation.repository.full_name !== env.REPO_TO_RUN_ON) {
return;
} else if (env.REPO_TO_RUN_ON !== undefined) {
console.log("REPO_TO_RUN_ON has been found, running logic.");
}
console.log("Encountered installation", installation.repository.full_name);
await updateGeneratorVersionInternal(
installation.octokit,
installation.repository,
env.GITHUB_APP_LOGIN_NAME,
env.GITHUB_APP_LOGIN_ID,
);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { evaluateEnv } from "@libs/env";
import { handlerWrapper } from "@libs/handler-wrapper";
import { updateGeneratorVersionsInternal } from "./actions/updateGeneratorVersions";

const updateGeneratorVersions = async (_event: unknown) => {
console.debug("Beginning scheduled run of `updateGeneratorVersions`, received event:", _event);
const env = evaluateEnv();
console.debug("Environment evaluated, continuing to actual action execution.");
return updateGeneratorVersionsInternal(env);
};

export const handler = handlerWrapper(updateGeneratorVersions);
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { AbsoluteFilePath, doesPathExist } from "@fern-api/fs-utils";
import { generateChangelog, generateCommitMessage } from "@libs/cohere";
import { Env } from "@libs/env";
import { execFernCli } from "@libs/fern";
import { setupGithubApp } from "@libs/github/octokit";
import {
DEFAULT_REMOTE_NAME,
cloneRepo,
configureGit,
createOrUpdatePullRequest,
getOrUpdateBranch,
} from "@libs/github/utilities";
import { components } from "@octokit/openapi-types";
import { mkdir } from "fs/promises";
import { App, Octokit } from "octokit";
import * as path from "path";
import simpleGit from "simple-git";
import tmp from "tmp-promise";
import { execFernCli } from "../../../libs/fern";
import { setupGithubApp } from "../github/octokit";
import { createOrUpdatePullRequest } from "../github/utilities";

const OPENAPI_UPDATE_BRANCH = "fern/update-api-specs";
type Repository = components["schemas"]["repository"];
Expand All @@ -20,43 +21,12 @@ async function updateOpenApiSpecInternal(
fernBotLoginName: string,
fernBotLoginId: string,
): Promise<void> {
const tmpDir = await tmp.dir();
const fullRepoPath = AbsoluteFilePath.of(path.join(tmpDir.path, repository.id.toString(), repository.name));

const branchRemoteName = "origin";
const originDefaultBranch = `${branchRemoteName}/${repository.default_branch}`;

const [git, fullRepoPath] = await configureGit(repository);
console.log(`Cloning repo: ${repository.clone_url} to ${fullRepoPath}`);
if (!(await doesPathExist(fullRepoPath))) {
await mkdir(fullRepoPath, { recursive: true });
}
await cloneRepo(git, repository, octokit, fernBotLoginName, fernBotLoginId);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const installationToken = ((await octokit.auth({ type: "installation" })) as any).token;
const git = simpleGit(fullRepoPath);

const authedCloneUrl = repository.clone_url.replace("https://", `https://x-access-token:${installationToken}@`);
// Clone the repo to fullRepoPath and update the branch
await git.clone(authedCloneUrl, ".");
// Configure git to show the app as the committer
await git.addConfig("user.name", fernBotLoginName);
await git.addConfig("user.email", `${fernBotLoginId}+${fernBotLoginName}@users.noreply.github.com`);
try {
// If you can fetch the branch, checkout the branch
await git.fetch(branchRemoteName, OPENAPI_UPDATE_BRANCH);
console.log("Branch exists, checking out");
await git.checkout(OPENAPI_UPDATE_BRANCH);
// Merge the default branch into this branch to update it
// prefer the default branch changes
//
// TODO: we could honestly probably just delete the branch and recreate it
// my concern with that is if there are more changes we decide to make in other actions
// to the same branch that are not OpenAPI related, that we'd lose if we deleted and reupdated the spec.
await git.merge(["-X", "theirs", originDefaultBranch]);
} catch (e) {
console.log("Branch does not exist, create and checkout");
await git.checkoutBranch(OPENAPI_UPDATE_BRANCH, branchRemoteName);
}
const originDefaultBranch = `${DEFAULT_REMOTE_NAME}/${repository.default_branch}`;
await getOrUpdateBranch(git, originDefaultBranch, OPENAPI_UPDATE_BRANCH);

try {
// Run API update command which will pull the new spec from the specified
Expand All @@ -77,7 +47,7 @@ async function updateOpenApiSpecInternal(
// Push the changes
await git.push([
"--force-with-lease",
branchRemoteName,
DEFAULT_REMOTE_NAME,
`${OPENAPI_UPDATE_BRANCH}:refs/heads/${OPENAPI_UPDATE_BRANCH}`,
]);

Expand Down
89 changes: 0 additions & 89 deletions servers/fern-bot/src/functions/oas-cron/github/utilities.ts

This file was deleted.

6 changes: 0 additions & 6 deletions servers/fern-bot/src/functions/oas-cron/mock.json

This file was deleted.

7 changes: 0 additions & 7 deletions servers/fern-bot/src/functions/oas-cron/schema.ts

This file was deleted.

Loading
Loading