From 990231841aaf231548fcecab652832e02001f781 Mon Sep 17 00:00:00 2001
From: Philipp Dollst <me@iamphilipp.com>
Date: Thu, 22 Aug 2024 17:54:12 +0200
Subject: [PATCH] feat: add build envs similar to standard git integration
 (#27)

---
 README.md                                  | 20 +++++
 vercel-deployment-task-source/src/index.ts | 87 +++++++++++++++-------
 vercel-deployment-task-source/task.json    |  2 +-
 vss-extension.json                         |  2 +-
 4 files changed, 84 insertions(+), 27 deletions(-)

diff --git a/README.md b/README.md
index d97b2c6..71a8cc6 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,8 @@ This extension contains Azure Pipelines tasks for automatically deploying your A
   - [Extension Set Up](#extension-set-up)
   - [Basic Pipeline Set Up](#basic-pipeline-set-up)
   - [Full Featured Pipeline Set Up](#full-featured-pipeline-set-up)
+  - [Env Vars](#env-vars)
+    - [Available Env Vars](#available-env-vars)
   - [Extension Reference](#extension-reference)
     - [Task: `vercel-deployment-task`](#task-vercel-deployment-task)
     - [Task: `vercel-azdo-pr-comment-task`](#task-vercel-azdo-pr-comment-task)
@@ -92,6 +94,24 @@ This guide will demonstrate how to improve the [Basic Pipeline Set Up](#basic-pi
 1. Push these changes to the repository, and set a [Build Policy](#azure-build-policy-set-up) for the `main` branch.
 1. Now create a new branch, push a commit, and open a PR against `main`. A new pipeline execution should trigger and it should create a preview deployment on Vercel as well as comment back on the PR with the preview URL.
 
+## Env Vars
+
+The extension provides a set of env vars if the option [Automatically exposing System Environment Variables](https://vercel.com/docs/projects/environment-variables/system-environment-variables) is enabled in the Project Settings. These vars are available in the build step and at run time.
+
+Based on the selected [Framework Preset](https://vercel.com/docs/deployments/configure-a-build#framework-preset) framework-specific env vars are set for the build step like with a [regular integration](https://vercel.com/docs/projects/environment-variables/system-environment-variables#framework-environment-variables).
+   > Note: This option is currently only available for Next.js.
+
+### Available Env Vars
+
+| DevOps Variable                                        | Generic                     | Next.js                                |
+| ------------------------------------------------------ | --------------------------- | -------------------------------------- |
+| Build.SourceVersion                                    | DEVOPS_GIT_COMMIT_SHA       | NEXT_PUBLIC_DEVOPS_GIT_COMMIT_SHA      |
+| System.PullRequest.SourceBranch/Build.SourceBranchName | DEVOPS_GIT_COMMIT_REF       | NEXT_PUBLIC_DEVOPS_GIT_COMMIT_REF      |
+| System.PullRequest.PullRequestId                       | DEVOPS_GIT_PULL_REQUEST_ID  | NEXT_PUBLIC_DEVOPS_GIT_PULL_REQUEST_ID |
+| "devops"                                               | DEVOPS_GIT_PROVIDER         | NEXT_PUBLIC_DEVOPS_GIT_PROVIDER        |
+| System.TeamProjectId                                   | DEVOPS_GIT_REPO_ID          | NEXT_PUBLIC_DEVOPS_GIT_REPO_ID         |
+| System.TeamProject                                     | DEVOPS_GIT_REPO_SLUG        | NEXT_PUBLIC_DEVOPS_GIT_REPO_SLUG       |
+
 ## Extension Reference
 
 ### Task: `vercel-deployment-task`
diff --git a/vercel-deployment-task-source/src/index.ts b/vercel-deployment-task-source/src/index.ts
index b613068..f6a9d72 100644
--- a/vercel-deployment-task-source/src/index.ts
+++ b/vercel-deployment-task-source/src/index.ts
@@ -47,11 +47,15 @@ async function getStagingPrefix(orgID: string, token: string): Promise<string> {
   return isTeam ? result.stagingPrefix : result.user.stagingPrefix;
 }
 
-async function getProjectName(
+// https://vercel.com/docs/rest-api/endpoints/projects#find-a-project-by-id-or-name-response
+type Framework = 'blitzjs' | 'nextjs' | 'gatsby' | 'remix' | 'astro' | 'hexo' | 'eleventy' | 'docusaurus-2' | 'docusaurus' | 'preact' | 'solidstart-1' | 'solidstart' | 'dojo' | 'ember' | 'vue' | 'scully' | 'ionic-angular' | 'angular' | 'polymer' | 'svelte' | 'sveltekit' | 'sveltekit-1' | 'ionic-react' | 'create-react-app' | 'gridsome' | 'umijs' | 'sapper' | 'saber' | 'stencil' | 'nuxtjs' | 'redwoodjs' | 'hugo' | 'jekyll' | 'brunch' | 'middleman' | 'zola' | 'hydrogen' | 'vite' | 'vitepress' | 'vuepress' | 'parcel' | 'fasthtml' | 'sanity' | 'storybook' | null;
+type Project = { autoExposeSystemEnvs: boolean; framework: Framework; name: string; }
+
+async function getProject(
   projectId: string,
   orgId: string,
   token: string
-): Promise<string> {
+): Promise<Project> {
   let apiURL = `https://api.vercel.com/v9/projects/${projectId}`;
   if (isTeamID(orgId)) {
     apiURL += `?teamId=${orgId}`;
@@ -72,7 +76,7 @@ async function getProjectName(
     );
   }
 
-  return result.name;
+  return result;
 }
 
 /**
@@ -195,6 +199,56 @@ async function run() {
     if (archive) {
       vercelDeployArgs.push("--archive=tgz");
     }
+
+    const project = await getProject(vercelProjectId, vercelOrgId, vercelToken)
+
+    // Get branch name
+    // If triggered by a PR use `System.PullRequest.SourceBranch` (and replace the `refs/heads/`)
+    // If not triggered by a PR use `Build.SourceBranchName`
+    let branchName: string | undefined;
+    const buildReason = getVariable("Build.Reason");
+    if (buildReason === "PullRequest") {
+      branchName = getVariable("System.PullRequest.SourceBranch");
+      if (branchName) {
+        branchName = branchName.replace("refs/heads/", "");
+      }
+    } else {
+      branchName = getVariable("Build.SourceBranchName");
+    }
+
+    // adding predefined DevOps variables which can be useful during build as env vars in a similiar style as the regular Vercel git integration would (replacing VERCEL with DEVOPS)
+    if (project.autoExposeSystemEnvs) {
+      const addEnvVar = (envVar: string) => {
+        vercelDeployArgs.push(`--build-env ${envVar}`);
+        vercelDeployArgs.push(`--env ${envVar}`);
+      }
+
+      const commitSha = getVariable("Build.SourceVersion");
+      const pullRequestId = getVariable("System.PullRequest.PullRequestId");
+      const teamProject = getVariable("System.TeamProject");
+      const teamProjectId = getVariable("System.TeamProjectId");
+
+      addEnvVar(`DEVOPS_GIT_COMMIT_SHA=${commitSha}`);
+      addEnvVar(`DEVOPS_GIT_COMMIT_REF=${branchName}`);
+      addEnvVar(`DEVOPS_GIT_PULL_REQUEST_ID=${pullRequestId}`);
+      addEnvVar(`DEVOPS_GIT_PROVIDER=devops`);
+      addEnvVar(`DEVOPS_GIT_REPO_ID=${teamProjectId}`);
+      addEnvVar(`DEVOPS_GIT_REPO_SLUG=${teamProject}`);
+
+      // adding framework specific vars as with regular integration (currently only Next.js is supported) https://vercel.com/docs/projects/environment-variables/system-environment-variables#framework-environment-variables 
+      switch (project.framework) {
+        case 'nextjs':
+          vercelDeployArgs.push(`--build-env NEXT_PUBLIC_DEVOPS_GIT_COMMIT_SHA=${commitSha}`);
+          vercelDeployArgs.push(`--build-env NEXT_PUBLIC_DEVOPS_GIT_COMMIT_REF=${branchName}`);
+          vercelDeployArgs.push(`--build-env NEXT_PUBLIC_DEVOPS_GIT_PULL_REQUEST_ID=${pullRequestId}`);
+          vercelDeployArgs.push(`--build-env NEXT_PUBLIC_DEVOPS_GIT_PROVIDER=devops`);
+          vercelDeployArgs.push(`--build-env NEXT_PUBLIC_DEVOPS_GIT_REPO_ID=${teamProjectId}`);
+          vercelDeployArgs.push(`--build-env NEXT_PUBLIC_DEVOPS_GIT_REPO_SLUG=${teamProject}`);
+          break;
+      }
+    }
+
+
     const vercelDeploy = vercel.arg(vercelDeployArgs);
     ({ stdout, stderr, code } = vercelDeploy.execSync());
 
@@ -207,25 +261,8 @@ async function run() {
     let deployURL = stdout;
 
     if (!deployToProduction) {
-      // Get branch name
-      // If triggered by a PR use `System.PullRequest.SourceBranch` (and replace the `refs/heads/`)
-      // If not triggered by a PR use `Build.SourceBranchName`
-      let branchName: string | undefined;
-      const buildReason = getVariable("Build.Reason");
-      if (buildReason && buildReason === "PullRequest") {
-        branchName = getVariable("System.PullRequest.SourceBranch");
-        if (branchName) {
-          branchName = branchName.replace("refs/heads/", "");
-        }
-      } else {
-        branchName = getVariable("Build.SourceBranchName");
-      }
-
       if (branchName) {
-        const [projectName, stagingPrefix] = await Promise.all([
-          getProjectName(vercelProjectId, vercelOrgId, vercelToken),
-          getStagingPrefix(vercelOrgId, vercelToken),
-        ]);
+        const stagingPrefix = await getStagingPrefix(vercelOrgId, vercelToken);
         const escapedBranchName = branchName.replace(/[^a-zA-Z0-9\-]-?/g, "-");
         /**
          * Truncating branch name according to RFC 1035 if necessary
@@ -233,7 +270,7 @@ async function run() {
          *
          * Read more: https://vercel.com/guides/why-is-my-vercel-deployment-url-being-shortened
          *
-         * projectName has a fixedLength `x`
+         * project.name has a fixedLength `x`
          * stagingPrefix has a fixedLenght `y`
          * .vercel.app has a fixedLength `11`
          * two dashes
@@ -254,8 +291,8 @@ async function run() {
          *    longer-project-name-feature-prefix-12346-my-second-f.vercel.app
          */
         const branchNameAllowedLength =
-          50 - projectName.length - stagingPrefix.length;
-        let aliasHostname = `${projectName}-${escapedBranchName}-${stagingPrefix}.vercel.app`;
+          50 - project.name.length - stagingPrefix.length;
+        let aliasHostname = `${project.name}-${escapedBranchName}-${stagingPrefix}.vercel.app`;
 
         if (escapedBranchName.length > branchNameAllowedLength) {
           // Calculate the maximum length of the branchName by removing the stagingPrefix and the dash
@@ -276,7 +313,7 @@ async function run() {
           }
 
           // Remove the stagingPrefix from the aliasHostname and use the extended aliasingBranchName
-          aliasHostname = `${projectName}-${aliasingBranchName}.vercel.app`;
+          aliasHostname = `${project.name}-${aliasingBranchName}.vercel.app`;
         }
 
         deployURL = `https://${aliasHostname}`;
diff --git a/vercel-deployment-task-source/task.json b/vercel-deployment-task-source/task.json
index c74f9c4..215a452 100644
--- a/vercel-deployment-task-source/task.json
+++ b/vercel-deployment-task-source/task.json
@@ -10,7 +10,7 @@
   "author": "Vercel",
   "version": {
     "Major": 1,
-    "Minor": 3,
+    "Minor": 4,
     "Patch": 0
   },
   "instanceNameFormat": "Deploying $(vercelProject) to Vercel",
diff --git a/vss-extension.json b/vss-extension.json
index 40d69fb..85d61c7 100644
--- a/vss-extension.json
+++ b/vss-extension.json
@@ -3,7 +3,7 @@
   "manifestVersion": 1,
   "id": "vercel-deployment-extension",
   "name": "Vercel Deployment Extension",
-  "version": "1.3.1",
+  "version": "1.4.0",
   "publisher": "Vercel",
   "public": true,
   "targets": [