From 7e0177c165700d6646caa9f3449f58461f9c5404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= Date: Wed, 17 Apr 2024 10:59:32 +0200 Subject: [PATCH] Setup `rerun.io` deploy from `docs-latest` (#5988) ### What Right now the workflow for updating docs on `rerun.io` is: 1. Push your changes to some branch in this repository 2. Get the hash of your latest commit 3. Login to our project on Vercel 4. Manually update the `RELEASE_COMMIT` env var 5. Trigger a redeploy of the latest production deployment After this PR, the new workflow will be: 1. Cherry-pick any docs commits to the `docs-latest` branch `rerun.io` now always fetches docs from the `docs-latest` branch, instead of a specific commit hash. Any push to `docs-latest` will trigger a redeploy of the website. The release workflow has been updated to no longer deploy the website, and instead only update the `RELEASE_VERSION` env var. This PR also contains a refactor of the `deploy-vercel` action, removing some code duplication and simplifying its usage locally through `manual.mjs`. ### Checklist * [x] I have read and agree to [Contributor Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and the [Code of Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md) * [x] I've included a screenshot or gif (if applicable) * [x] I have tested the web demo (if applicable): * Using examples from latest `main` build: [rerun.io/viewer](https://rerun.io/viewer/pr/5988?manifest_url=https://app.rerun.io/version/main/examples_manifest.json) * Using full set of examples from `nightly` build: [rerun.io/viewer](https://rerun.io/viewer/pr/5988?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json) * [x] The PR title and labels are set such as to maximize their usefulness for the next release's CHANGELOG * [x] If applicable, add a new check to the [release checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)! - [PR Build Summary](https://build.rerun.io/pr/5988) - [Recent benchmark results](https://build.rerun.io/graphs/crates.html) - [Wasm size tracking](https://build.rerun.io/graphs/sizes.html) To run all checks from `main`, comment on the PR with `@rerun-bot full-check`. --- .github/actions/deploy-vercel/index.mjs | 32 -- .github/actions/deploy-vercel/manual.mjs | 48 --- .github/actions/deploy-vercel/preview.mjs | 49 --- .github/actions/deploy-vercel/production.mjs | 68 ---- .github/actions/deploy-vercel/test.mjs | 38 --- .../{deploy-vercel => vercel}/action.yml | 8 +- .../vercel/commands/deploy-preview.mjs | 36 +++ .../vercel/commands/deploy-production.mjs | 23 ++ .../actions/vercel/commands/update-env.mjs | 20 ++ .github/actions/vercel/index.mjs | 72 +++++ .github/actions/vercel/manual.mjs | 73 +++++ .../{deploy-vercel => vercel}/util.mjs | 24 +- .../{deploy-vercel => vercel}/vercel.mjs | 303 ++++++++++-------- .github/workflows/on_push_docs.yml | 25 ++ .github/workflows/release.yml | 6 +- .github/workflows/reusable_deploy_docs.yml | 9 +- .../reusable_deploy_landing_preview.yml | 7 +- docs/README.md | 8 + 18 files changed, 450 insertions(+), 399 deletions(-) delete mode 100644 .github/actions/deploy-vercel/index.mjs delete mode 100755 .github/actions/deploy-vercel/manual.mjs delete mode 100644 .github/actions/deploy-vercel/preview.mjs delete mode 100644 .github/actions/deploy-vercel/production.mjs delete mode 100644 .github/actions/deploy-vercel/test.mjs rename .github/actions/{deploy-vercel => vercel}/action.yml (91%) create mode 100644 .github/actions/vercel/commands/deploy-preview.mjs create mode 100644 .github/actions/vercel/commands/deploy-production.mjs create mode 100644 .github/actions/vercel/commands/update-env.mjs create mode 100644 .github/actions/vercel/index.mjs create mode 100755 .github/actions/vercel/manual.mjs rename .github/actions/{deploy-vercel => vercel}/util.mjs (77%) rename .github/actions/{deploy-vercel => vercel}/vercel.mjs (64%) create mode 100644 .github/workflows/on_push_docs.yml diff --git a/.github/actions/deploy-vercel/index.mjs b/.github/actions/deploy-vercel/index.mjs deleted file mode 100644 index b306ef8c2fb3..000000000000 --- a/.github/actions/deploy-vercel/index.mjs +++ /dev/null @@ -1,32 +0,0 @@ -// @ts-check - -import { Client } from "./vercel.mjs"; -import { assert, getInput, getRequiredInput } from "./util.mjs"; -import { deployToProduction } from "./production.mjs"; -import { deployToPreview } from "./preview.mjs"; - -// These inputs are defined in `action.yml`, and should be kept in sync -const token = getRequiredInput("vercel_token"); -const team = getRequiredInput("vercel_team_name"); -const project = getRequiredInput("vercel_project_name"); -const commit = getRequiredInput("release_commit"); -const target = getRequiredInput("target"); - -const client = new Client(token); - -switch (target) { - case "production": { - const version = getRequiredInput("release_version"); - await deployToProduction(client, { team, project, commit, version }); - break; - } - - case "preview": { - await deployToPreview(client, { team, project, commit }); - break; - } - - default: { - throw new Error(`"target" must be one of: production, preview`); - } -} diff --git a/.github/actions/deploy-vercel/manual.mjs b/.github/actions/deploy-vercel/manual.mjs deleted file mode 100755 index 74232ada93cd..000000000000 --- a/.github/actions/deploy-vercel/manual.mjs +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env node - -// Manually run the deployment: -// -// node manual.mjs \ -// --token VERCEL_TOKEN \ -// --team rerun \ -// --project landing \ -// --commit RELEASE_COMMIT -// - -import { execSync } from "node:child_process"; -import { parseArgs } from "node:util"; -import { fileURLToPath } from "node:url"; -import path from "node:path"; -import { assert } from "./util.mjs"; - -const dirname = path.dirname(fileURLToPath(import.meta.url)); - -/** @type {typeof execSync} */ -const $ = (cmd, opts) => execSync(cmd, { stdio: "inherit", ...opts }); - -const { token, team, project, commit } = parseArgs({ - options: { - token: { type: "string" }, - team: { type: "string" }, - project: { type: "string" }, - commit: { type: "string" }, - }, - strict: true, - allowPositionals: false, -}).values; -assert(token, "missing `--token`"); -assert(team, "missing `--team`"); -assert(project, "missing `--project`"); -assert(commit, "missing `--commit`"); - -$("node index.mjs", { - cwd: dirname, - env: { - ...process.env, - INPUT_VERCEL_TOKEN: token, - INPUT_VERCEL_TEAM_NAME: team, - INPUT_VERCEL_PROJECT_NAME: project, - INPUT_RELEASE_COMMIT: commit, - }, -}); - diff --git a/.github/actions/deploy-vercel/preview.mjs b/.github/actions/deploy-vercel/preview.mjs deleted file mode 100644 index 1ff791694d98..000000000000 --- a/.github/actions/deploy-vercel/preview.mjs +++ /dev/null @@ -1,49 +0,0 @@ -// @ts-check - -import { assert, info, setOutput } from "./util.mjs"; -import { Client } from "./vercel.mjs"; - -/** - * - * @param {Client} client - * @param {{ - * team: string; - * project: string; - * commit: string; - * }} options - */ -export async function deployToPreview(client, options) { - info`Fetching team "${options.team}"`; - const availableTeams = await client.teams(); - assert(availableTeams, `failed to get team "${options.team}"`); - const team = availableTeams.find((team) => team.name === options.team); - assert(team, `failed to get team "${options.team}"`); - - info`Fetching project "${options.project}"`; - const projectsInTeam = await client.projects(team.id); - const project = projectsInTeam.find( - (project) => project.name === options.project, - ); - assert(project, `failed to get project "${options.project}"`); - - info`Fetching latest production deployment`; - const productionDeployments = await client.deployments(team.id, project.id); - const latestProductionDeployment = productionDeployments[0]; - assert( - latestProductionDeployment, - `failed to get latest production deployment`, - ); - - info`Deploying preview with RELEASE_COMMIT=${options.commit}`; - const { url } = await client.deployPreviewFrom( - team.id, - latestProductionDeployment.uid, - "landing-preview", - { - RELEASE_COMMIT: options.commit, - IS_PR_PREVIEW: "true", - }, - ); - - setOutput("vercel_preview_url", url); -} diff --git a/.github/actions/deploy-vercel/production.mjs b/.github/actions/deploy-vercel/production.mjs deleted file mode 100644 index f73b462850a4..000000000000 --- a/.github/actions/deploy-vercel/production.mjs +++ /dev/null @@ -1,68 +0,0 @@ -// @ts-check - -import { assert, info } from "./util.mjs"; -import { Client } from "./vercel.mjs"; - -/** - * - * @param {Client} client - * @param {{ - * team: string; - * project: string; - * commit: string; - * version: string; - * }} options - */ -export async function deployToProduction(client, options) { - info`Fetching team "${options.team}"`; - const availableTeams = await client.teams(); - assert(availableTeams, `failed to get team "${options.team}"`); - const team = availableTeams.find((team) => team.name === options.team); - assert(team, `failed to get team "${options.team}"`); - - info`Fetching project "${options.project}"`; - const projectsInTeam = await client.projects(team.id); - const project = projectsInTeam.find( - (project) => project.name === options.project, - ); - assert(project, `failed to get project "${options.project}"`); - - info`Fetching latest production deployment`; - const productionDeployments = await client.deployments(team.id, project.id); - const latestProductionDeployment = productionDeployments[0]; - assert( - latestProductionDeployment, - `failed to get latest production deployment`, - ); - - const environment = await client.envs(team.id, project.id); - const RELEASE_COMMIT_KEY = "RELEASE_COMMIT"; - const RELEASE_VERSION_KEY = "RELEASE_VERSION"; - - info`Fetching "${RELEASE_COMMIT_KEY}" env var`; - const releaseCommitEnv = environment.find( - (env) => env.key === RELEASE_COMMIT_KEY, - ); - assert(releaseCommitEnv, `failed to get "${RELEASE_COMMIT_KEY}" env var`); - - info`Fetching "${RELEASE_VERSION_KEY}" env var`; - const releaseVersionEnv = environment.find( - (env) => env.key === RELEASE_VERSION_KEY, - ); - assert(releaseVersionEnv, `failed to get "${RELEASE_VERSION_KEY}" env var`); - - info`Setting "${RELEASE_COMMIT_KEY}" env to "${options.commit}"`; - await client.setEnv(team.id, project.id, releaseCommitEnv.id, { - key: RELEASE_COMMIT_KEY, - value: options.commit, - }); - - info`Setting "${RELEASE_VERSION_KEY}" env to "${options.version}"`; - await client.setEnv(team.id, project.id, releaseVersionEnv.id, { - key: RELEASE_VERSION_KEY, - value: options.version, - }); - - info`Triggering redeploy`; - await client.redeploy(team.id, latestProductionDeployment.uid, "landing"); -} diff --git a/.github/actions/deploy-vercel/test.mjs b/.github/actions/deploy-vercel/test.mjs deleted file mode 100644 index a4d63265d3be..000000000000 --- a/.github/actions/deploy-vercel/test.mjs +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-check - -import { Client } from "./vercel.mjs"; -import { assert, info } from "./util.mjs"; - -const client = new Client("NzuZ9WBTnfUGiwHrhd7mit2E"); - -const teamName = "rerun"; -const projectName = "landing"; - -info`Fetching team "${teamName}"`; -const availableTeams = await client.teams(); -assert(availableTeams, `failed to get team "${teamName}"`); -const team = availableTeams.find((team) => team.name === teamName); -assert(team, `failed to get team "${teamName}"`); - -info`Fetching project "${projectName}"`; -const projectsInTeam = await client.projects(team.id); -const project = projectsInTeam.find((project) => project.name === projectName); -assert(project, `failed to get project "${projectName}"`); - -info`Fetching latest production deployment`; -const productionDeployments = await client.deployments(team.id, project.id); -const latestProductionDeployment = productionDeployments[0]; -assert( - latestProductionDeployment, - `failed to get latest production deployment`, -); - -const response = await client.deployPreviewFrom( - team.id, - latestProductionDeployment.uid, - "rerun-custom-preview-test", - { - RELEASE_COMMIT: "main", - }, -); -console.log(response); diff --git a/.github/actions/deploy-vercel/action.yml b/.github/actions/vercel/action.yml similarity index 91% rename from .github/actions/deploy-vercel/action.yml rename to .github/actions/vercel/action.yml index c33cc346b7e5..727ea7dbc552 100644 --- a/.github/actions/deploy-vercel/action.yml +++ b/.github/actions/vercel/action.yml @@ -22,10 +22,14 @@ inputs: description: "Vercel project name to update and redeploy" type: string required: true + command: + description: "`deploy` or `update-env`" + type: string + required: true release_commit: description: "Release commit to update the deployment to" type: string - required: true + required: false release_version: description: "Which release version to update the deployment to" type: string @@ -33,7 +37,7 @@ inputs: target: description: "Which Vercel environment to deploy to" type: string - required: true + required: false runs: using: "node20" diff --git a/.github/actions/vercel/commands/deploy-preview.mjs b/.github/actions/vercel/commands/deploy-preview.mjs new file mode 100644 index 000000000000..c66a46b59a03 --- /dev/null +++ b/.github/actions/vercel/commands/deploy-preview.mjs @@ -0,0 +1,36 @@ +// @ts-check + +import { setOutput } from "../util.mjs"; +import { Client } from "../vercel.mjs"; + +/** + * + * @param {Client} client + * @param {{ + * team: string; + * project: string; + * commit: string | null; + * version: string | null; + * }} options + */ +export async function deployToPreview(client, options) { + const project = await client.project(options.team, options.project); + const deployment = await project.latestProductionDeployment(); + + let line = `Deploying preview`; + if (options.commit) line += ` RELEASE_COMMIT=${options.commit}`; + if (options.version) line += ` RELEASE_VERSION=${options.version}`; + console.log(line); + + const env = { IS_PR_PREVIEW: "true" }; + if (options.commit) env["RELEASE_COMMIT"] = options.commit; + if (options.version) env["RELEASE_VERSION"] = options.version; + + const { url } = await project.deployPreviewFrom( + deployment.uid, + "landing-preview", + env, + ); + + setOutput("vercel_preview_url", url); +} diff --git a/.github/actions/vercel/commands/deploy-production.mjs b/.github/actions/vercel/commands/deploy-production.mjs new file mode 100644 index 000000000000..56541a13a50b --- /dev/null +++ b/.github/actions/vercel/commands/deploy-production.mjs @@ -0,0 +1,23 @@ +// @ts-check + +import { Client } from "../vercel.mjs"; + +/** + * + * @param {Client} client + * @param {{ + * team: string; + * project: string; + * commit: string | null; + * version: string | null; + * }} options + */ +export async function deployToProduction(client, options) { + const project = await client.project(options.team, options.project); + const deployment = await project.latestProductionDeployment(); + + if (options.commit) await project.setEnv("RELEASE_COMMIT", options.commit); + if (options.version) await project.setEnv("RELEASE_VERSION", options.version); + + await project.redeploy(deployment.uid, "landing"); +} diff --git a/.github/actions/vercel/commands/update-env.mjs b/.github/actions/vercel/commands/update-env.mjs new file mode 100644 index 000000000000..418aa4fc591d --- /dev/null +++ b/.github/actions/vercel/commands/update-env.mjs @@ -0,0 +1,20 @@ +// @ts-check + +import { Client } from "../vercel.mjs"; + +/** + * + * @param {Client} client + * @param {{ + * team: string; + * project: string; + * commit: string | null; + * version: string | null; + * }} options + */ +export async function updateProjectEnv(client, options) { + const project = await client.project(options.team, options.project); + + if (options.commit) await project.setEnv("RELEASE_COMMIT", options.commit); + if (options.version) await project.setEnv("RELEASE_VERSION", options.version); +} diff --git a/.github/actions/vercel/index.mjs b/.github/actions/vercel/index.mjs new file mode 100644 index 000000000000..810aa3037f2d --- /dev/null +++ b/.github/actions/vercel/index.mjs @@ -0,0 +1,72 @@ +// @ts-check + +import { Client } from "./vercel.mjs"; +import { getInput, getRequiredInput } from "./util.mjs"; +import { deployToProduction } from "./commands/deploy-production.mjs"; +import { deployToPreview } from "./commands/deploy-preview.mjs"; +import { updateProjectEnv } from "./commands/update-env.mjs"; + +// All inputs retrieved via `getInput` are defined in `action.yml`, and should be kept in sync + +const client = new Client(getRequiredInput("vercel_token")); + +const command = getRequiredInput("command"); +const team = getRequiredInput("vercel_team_name"); +const project = getRequiredInput("vercel_project_name"); + +switch (command) { + case "deploy": + await deploy(client, team, project); + break; + case "update-env": + await updateEnv(client, team, project); + break; + default: + throw new Error(`"command" must be one of: deploy, update-env`); +} + +/** + * @param {Client} client + * @param {string} team + * @param {string} project + */ +async function deploy(client, team, project) { + const target = getRequiredInput("target"); + switch (target) { + case "production": { + const commit = getRequiredInput("release_commit"); + const version = getRequiredInput("release_version"); + await deployToProduction(client, { team, project, commit, version }); + break; + } + + case "preview": { + const commit = getRequiredInput("release_commit"); + const version = getInput("release_version"); + await deployToPreview(client, { team, project, commit, version }); + break; + } + + default: { + throw new Error(`"target" must be one of: production, preview`); + } + } +} + +/** + * @param {Client} client + * @param {string} team + * @param {string} project + */ +async function updateEnv(client, team, project) { + const commit = getInput("release_commit"); + const version = getInput("release_version"); + + if (!commit && !version) { + throw new Error( + `one of "release_commit", "release_version" must be specified`, + ); + } + + await updateProjectEnv(client, { team, project, commit, version }); +} diff --git a/.github/actions/vercel/manual.mjs b/.github/actions/vercel/manual.mjs new file mode 100755 index 000000000000..aebe273cc9c9 --- /dev/null +++ b/.github/actions/vercel/manual.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +// Convenience script for running `index.mjs` locally, +// passing in inputs as CLI args instead of env vars. + +// @ts-check + +// Manually run the deployment: +// +// # Redeploy `landing`: +// node manual.mjs \ +// --command deploy \ +// --token VERCEL_TOKEN \ +// --team rerun \ +// --project landing \ +// --target production \ +// --commit RELEASE_COMMIT \ +// --version RELEASE_VERSION +// +// # Deploy a preview of `landing` with a `RELEASE_COMMIT` override: +// node manual.mjs \ +// --command deploy \ +// --token VERCEL_TOKEN \ +// --team rerun \ +// --project landing \ +// --target preview \ +// --commit RELEASE_COMMIT +// +// # Only update env: +// node manual.mjs \ +// --command update-env \ +// --token VERCEL_TOKEN \ +// --team rerun \ +// --project landing \ +// --commit RELEASE_COMMIT \ +// --version RELEASE_VERSION +// + +import { execSync } from "node:child_process"; +import { parseArgs } from "node:util"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +try { + const { command, token, team, project, target, commit, version } = parseArgs({ + options: { + command: { type: "string" }, + token: { type: "string" }, + team: { type: "string" }, + project: { type: "string" }, + target: { type: "string" }, + commit: { type: "string" }, + version: { type: "string" }, + }, + strict: true, + allowPositionals: false, + }).values; + + const env = { ...process.env, MANUAL_RUN: "true" }; + if (command) env["INPUT_COMMAND"] = command; + if (token) env["INPUT_VERCEL_TOKEN"] = token; + if (team) env["INPUT_VERCEL_TEAM_NAME"] = team; + if (project) env["INPUT_VERCEL_PROJECT_NAME"] = project; + if (target) env["INPUT_TARGET"] = target; + if (commit) env["INPUT_RELEASE_COMMIT"] = commit; + if (version) env["INPUT_RELEASE_VERSION"] = version; + + const cwd = path.dirname(fileURLToPath(import.meta.url)); + execSync("node index.mjs", { cwd, env, stdio: "inherit" }); +} catch (err) { + console.error(err.message); + process.exit(1); +} diff --git a/.github/actions/deploy-vercel/util.mjs b/.github/actions/vercel/util.mjs similarity index 77% rename from .github/actions/deploy-vercel/util.mjs rename to .github/actions/vercel/util.mjs index 24bb3d76db98..8e8a81fa9abc 100644 --- a/.github/actions/deploy-vercel/util.mjs +++ b/.github/actions/vercel/util.mjs @@ -3,23 +3,6 @@ import { appendFileSync } from "fs"; import os from "os"; -/** - * Log a message with level `INFO` - * - * @param {TemplateStringsArray} strings - * @param {any[]} values - */ -export function info(strings, ...values) { - let out = ""; - for (let i = 0; i < strings.length; i++) { - out += strings[i]; - if (i < values.length) { - out += values[i].toString(); - } - } - console.info(out); -} - /** * Return a GitHub Actions input, returning `null` if it was not set. * @@ -50,8 +33,13 @@ export function getRequiredInput(name) { * @param {string} value */ export function setOutput(key, value) { + const s = `${key}=${value}${os.EOL}`; + if (process.env["MANUAL_RUN"]) { + console.log(s); + return; + } const outputFile = /** @type {string} */ (process.env["GITHUB_OUTPUT"]); - appendFileSync(outputFile, `${key}=${value}${os.EOL}`); + appendFileSync(outputFile, s); } /** diff --git a/.github/actions/deploy-vercel/vercel.mjs b/.github/actions/vercel/vercel.mjs similarity index 64% rename from .github/actions/deploy-vercel/vercel.mjs rename to .github/actions/vercel/vercel.mjs index e69aab2a5fcb..1550c4ef4635 100644 --- a/.github/actions/deploy-vercel/vercel.mjs +++ b/.github/actions/vercel/vercel.mjs @@ -6,8 +6,8 @@ import { assert } from "./util.mjs"; * @typedef {Record} Headers * @typedef {object} Body * - * @typedef {{ id: string; name: string }} Team - * @typedef {{ id: string; name: string }} Project + * @typedef {{ id: string; name: string }} TeamInfo + * @typedef {{ id: string; name: string }} ProjectInfo * @typedef {{ uid: string }} Deployment * @typedef {{ id: string, key: string, value: string }} Env * @@ -15,6 +15,153 @@ import { assert } from "./util.mjs"; * @typedef {"encrypted" | "secret"} EnvType */ +export class Project { + constructor( + /** @type {Client} */ client, + /** @type {TeamInfo} */ team, + /** @type {ProjectInfo} */ project, + ) { + this.client = client; + this.team = team; + this.project = project; + } + + /** + * Return deployments under the current team and project. + * + * The endpoint used is a paginated one, but this call does not support pagination, + * and only returns the first 20 results. + * + * The results are sorted by their created date, so the latest deployment + * for the given `target` is at index `0`. + * @param {"production" | "preview" | "development"} target + * @returns {Promise} + */ + async deployments(target = "production") { + const response = await this.client.get("v6/deployments", { + teamId: this.team.id, + projectId: this.project.id, + target, + sort: "created", + }); + assert( + "deployments" in response, + () => `failed to get deployments: ${JSON.stringify(response)}`, + ); + return response.deployments; + } + + async latestProductionDeployment() { + console.log("get latest production deployment"); + const productionDeployments = await this.deployments("production"); + const latestProductionDeployment = productionDeployments[0]; + assert( + latestProductionDeployment, + `failed to get latest production deployment`, + ); + return latestProductionDeployment; + } + + /** + * Return environment variables available to the current team and project. + * + * @returns {Promise} + */ + async envs() { + const response = await this.client.get( + `v9/projects/${this.project.id}/env`, + { + teamId: this.team.id, + }, + ); + assert( + "envs" in response, + () => `failed to get environment variables: ${JSON.stringify(response)}`, + ); + return response.envs; + } + + /** + * Set an environment variable (`envId`), making it available to the current team and project. + * + * @param {string} key + * @param {string} value + * @param {EnvTarget[]} [target] + * @param {EnvType} [type] + * @returns {Promise} + */ + async setEnv( + key, + value, + target = ["production", "preview", "development"], + type = "encrypted", + ) { + console.log(`set env ${key}=${value} (target: ${target}, type: ${type})`); + const env = await this.envs().then((envs) => + envs.find((env) => env.key === key), + ); + assert(env); + return this.client.patch( + `v9/projects/${this.project.id}/env/${env.id}`, + { gitBranch: null, key, target, type, value }, + { teamId: this.team.id }, + ); + } + + /** + * Trigger a redeploy of an existing deployment (`deploymentId`) + * of a project (`name`) under a specific team (`teamId`). + * + * The resulting deployment will be set as the current production deployment. + * + * @param {string} deploymentId + * @param {string} name + * @returns {Promise} + */ + async redeploy(deploymentId, name) { + console.log(`redeploy ${name} (id: ${deploymentId})`); + return this.client.post( + `v13/deployments`, + { + deploymentId, + meta: { action: "redeploy" }, + name, + target: "production", + }, + { teamId: this.team.id, forceNew: "1" }, + ); + } + + /** + * Trigger a preview deploy using the files of an existing deployment (`deploymentId`). + * + * @param {string} deploymentId + * @param {string} name + * @param {Record} [env] + * @returns {Promise} + */ + async deployPreviewFrom(deploymentId, name, env) { + console.log( + `deploy preview from ${name} (id: ${deploymentId}) with env:`, + env, + ); + // `target` not being set means "preview" + const body = { + deploymentId, + meta: { action: "redeploy" }, + name, + }; + if (env) { + body.env = env; + body.build = { env }; + } + return this.client.post(`v13/deployments`, body, { + teamId: this.team.id, + forceNew: "1", + }); + } +} + /** * Vercel API Client */ @@ -47,7 +194,8 @@ export class Client { * @returns {Promise} */ async get(endpoint, params, headers) { - return await fetch(this.url(endpoint, params), { + const url = this.url(endpoint, params); + return fetch(url, { headers: { Authorization: `Bearer ${this.token}`, ...headers, @@ -66,7 +214,8 @@ export class Client { * @returns {Promise} */ async post(endpoint, body, params, headers) { - return await fetch(this.url(endpoint, params), { + const url = this.url(endpoint, params); + return fetch(url, { method: "POST", headers: { Authorization: `Bearer ${this.token}`, @@ -88,7 +237,8 @@ export class Client { * @returns {Promise} */ async patch(endpoint, body, params, headers) { - return await fetch(this.url(endpoint, params), { + const url = this.url(endpoint, params); + return fetch(url, { method: "PATCH", headers: { Authorization: `Bearer ${this.token}`, @@ -102,7 +252,7 @@ export class Client { /** * Return all available teams for the user authorized by this client's token. * - * @returns {Promise} + * @returns {Promise} */ async teams() { const response = await this.get("v2/teams"); @@ -118,7 +268,7 @@ export class Client { * for the user authorized by this client's token. * * @param {string} teamId - * @returns {Promise} + * @returns {Promise} */ async projects(teamId) { const response = await this.get("v9/projects", { teamId }); @@ -130,137 +280,20 @@ export class Client { } /** - * Return deployments under a given team (`teamId`) and project (`projectId`). - * - * The endpoint used is a paginated one, but this call does not support pagination, - * and only returns the first 20 results. - * - * The results are sorted by their created date, so the latest deployment - * for the given `target` is at index `0`. - * - * @param {string} teamId - * @param {string} projectId - * @param {"production" | "preview" | "development"} target - * @returns {Promise} - */ - async deployments(teamId, projectId, target = "production") { - const response = await this.get("v6/deployments", { - teamId, - projectId, - target, - sort: "created", - }); - assert( - "deployments" in response, - () => `failed to get deployments: ${JSON.stringify(response)}`, - ); - return response.deployments; - } - - /** - * Return environment variables available to a project (`projectId`) under a team (`teamId`). * - * @param {string} teamId - * @param {string} projectId - * @returns {Promise} + * @param {string} teamName + * @param {string} projectName */ - async envs(teamId, projectId) { - const response = await this.get(`v9/projects/${projectId}/env`, { teamId }); - assert( - "envs" in response, - () => `failed to get environment variables: ${JSON.stringify(response)}`, - ); - return response.envs; - } + async project(teamName, projectName) { + console.log(`get project ${teamName}/${projectName}`); - /** - * Get the decrypted version of an environment variable (`envId`) - * available to a project (`projectId`) under a team (`teamId`). - * - * @param {string} teamId - * @param {string} projectId - * @param {string} envId - * @returns {Promise} - */ - async getEnvDecrypted(teamId, projectId, envId) { - return await this.get(`v9/projects/${projectId}/env/${envId}`, { - teamId, - decrypt: "true", - }); - } + const teams = await this.teams(); + const team = teams.find((team) => team.name === teamName); + assert(team); + const projects = await this.projects(team.id); + const project = projects.find((project) => project.name === projectName); + assert(project); - /** - * Set an environment variable (`envId`), making it available to a project `projectId` - * under a team (`teamId`). - * - * @param {string} teamId - * @param {string} projectId - * @param {string} envId - * @param {{ key: string, target?: EnvTarget[], type?: EnvType, value: string }} param3 - * @returns {Promise} - */ - async setEnv( - teamId, - projectId, - envId, - { - key, - target = ["production", "preview", "development"], - type = "encrypted", - value, - }, - ) { - return await this.patch( - `v9/projects/${projectId}/env/${envId}`, - { gitBranch: null, key, target, type, value }, - { teamId }, - ); - } - - /** - * Trigger a redeploy of an existing deployment (`deploymentId`) - * of a project (`name`) under a specific team (`teamId`). - * - * The resulting deployment will be set as the current production deployment. - * - * @param {string} teamId - * @param {string} deploymentId - * @param {string} name - * @returns {Promise} - */ - async redeploy(teamId, deploymentId, name) { - return await this.post( - `v13/deployments`, - { - deploymentId, - meta: { action: "redeploy" }, - name, - target: "production", - }, - { teamId, forceNew: "1" }, - ); - } - - /** - * Trigger a preview deploy using the files of an existing deployment (`deploymentId`). - * - * @param {string} teamId - * @param {string} deploymentId - * @param {string} name - * @param {Record} [env] - * @returns {Promise} - */ - async deployPreviewFrom(teamId, deploymentId, name, env) { - // `target` not being set means "preview" - const body = { - deploymentId, - meta: { action: "redeploy" }, - name, - }; - if (env) { - body.env = env; - body.build = { env }; - } - return await this.post(`v13/deployments`, body, { teamId, forceNew: "1" }); + return new Project(this, team, project); } } diff --git a/.github/workflows/on_push_docs.yml b/.github/workflows/on_push_docs.yml new file mode 100644 index 000000000000..442e87c8582b --- /dev/null +++ b/.github/workflows/on_push_docs.yml @@ -0,0 +1,25 @@ +name: "Push Docs" + +on: + push: + branches: [docs-latest] + +concurrency: + group: on-push-docs + cancel-in-progress: true + +jobs: + redeploy-rerun-io: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Re-deploy rerun.io + uses: ./.github/actions/vercel + with: + command: "deploy" + vercel_token: ${{ secrets.VERCEL_TOKEN }} + vercel_team_name: ${{ vars.VERCEL_TEAM_NAME }} + vercel_project_name: ${{ vars.VERCEL_PROJECT_NAME }} + release_commit: "docs-latest" + target: "production" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9487b8e3dc97..113d13a09829 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -174,7 +174,7 @@ jobs: - If this is an 'alpha' release, you can just merge the pull request. - Otherwise: - For any added commits, run the release workflow in 'rc' mode again - - After testing, run the release workflow in 'release' mode + - After testing, _ensure that this PR is mergeable to `main`_, then run the release workflow in 'release' mode - Once the final release workflow finishes it will create a GitHub release for you. Then: - [ ] Sanity check the build artifacts: - [ ] pip install: does it install and run? @@ -184,6 +184,7 @@ jobs: - [ ] Make sure the [conda feedstock PR](https://github.com/conda-forge/rerun-sdk-feedstock/pulls) gets merged. This will be created by the `regro-cf-autotick-bot` once the GitHub release is created. - [ ] Update the [google colab notebooks](https://drive.google.com/drive/folders/0AC0q24MFKh3fUk9PVA) to install this version and re-execute the notebook. + - [ ] Merge this PR - [ ] Tests - [ ] Windows @@ -274,6 +275,8 @@ jobs: concurrency: ${{ github.ref_name }} secrets: inherit + # Force-pushes `latest` and `docs-latest` to the contents of the release branch. + # The push to `docs-latest` also triggers a re-deploy of `rerun.io`. update-latest-branch: name: "Update Latest Branch" if: inputs.release-type == 'final' @@ -302,6 +305,7 @@ jobs: git fetch git checkout ${{ github.ref_name }} git push --force origin refs/heads/${{ github.ref_name }}:refs/heads/latest + git push --force origin refs/heads/${{ github.ref_name }}:refs/heads/docs-latest github-release: name: "GitHub Release" diff --git a/.github/workflows/reusable_deploy_docs.yml b/.github/workflows/reusable_deploy_docs.yml index e7e14ca988c0..d38ddf52a5e3 100644 --- a/.github/workflows/reusable_deploy_docs.yml +++ b/.github/workflows/reusable_deploy_docs.yml @@ -245,7 +245,7 @@ jobs: git fetch python3 -m ghp_import -n -p -x docs/cpp/${{ inputs.CPP_DOCS_VERSION_NAME }} rerun_cpp/docs/html/ -m "Update the C++ docs" - redeploy-rerun-io: + update-rerun-io-env: runs-on: ubuntu-latest if: inputs.UPDATE_LATEST steps: @@ -253,12 +253,11 @@ jobs: with: ref: ${{ inputs.RELEASE_COMMIT || (github.event_name == 'pull_request' && github.event.pull_request.head.ref || '') }} - - name: Re-deploy rerun.io - uses: ./.github/actions/deploy-vercel + - name: Update Vercel env + uses: ./.github/actions/vercel with: + command: "update-env" vercel_token: ${{ secrets.VERCEL_TOKEN }} vercel_team_name: ${{ vars.VERCEL_TEAM_NAME }} vercel_project_name: ${{ vars.VERCEL_PROJECT_NAME }} - release_commit: ${{ inputs.RELEASE_COMMIT }} release_version: ${{ inputs.RELEASE_VERSION }} - target: "production" diff --git a/.github/workflows/reusable_deploy_landing_preview.yml b/.github/workflows/reusable_deploy_landing_preview.yml index e5e9abe85d17..dcdcb165ad5d 100644 --- a/.github/workflows/reusable_deploy_landing_preview.yml +++ b/.github/workflows/reusable_deploy_landing_preview.yml @@ -38,9 +38,10 @@ jobs: echo "sha=$full_commit" >> "$GITHUB_OUTPUT" - name: Re-deploy rerun.io - id: deploy-vercel - uses: ./.github/actions/deploy-vercel + id: vercel + uses: ./.github/actions/vercel with: + command: "deploy" vercel_token: ${{ secrets.VERCEL_TOKEN }} vercel_team_name: ${{ vars.VERCEL_TEAM_NAME }} vercel_project_name: ${{ vars.VERCEL_PROJECT_NAME }} @@ -57,4 +58,4 @@ jobs: | Commit | Link | | ------- | ----- | - | ${{ steps.get-sha.outputs.sha }} | https://${{ steps.deploy-vercel.outputs.vercel_preview_url }}/docs | + | ${{ steps.get-sha.outputs.sha }} | https://${{ steps.vercel.outputs.vercel_preview_url }}/docs | diff --git a/docs/README.md b/docs/README.md index 3b0bd54450f4..5a2bd6173fa3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,14 @@ The site documentation lives in Markdown files inside [`/content`](./content). The entry point to the documentation is [`/content/index.md`](./content/index.md). +## Updating the docs + +The `rerun.io` docs are built from the contents of the `/docs` directory on the `docs-latest` branch. Any push to `docs-latest` will trigger an automatic redeploy of the website. + +Do not push directly to the `docs-latest` branch! To update the docs, either [create a Release](../RELEASES.md), or cherry-pick commits to the `docs-latest` branch _after_ they've been committed to `main`. + +⚠ Any commits which are not on `main` and were instead submitted directly to the `docs-latest` branch will be lost the next time we create a release, because the `docs-latest` branch is force-pushed during the release process. + ## Special syntax ### Frontmatter