This guide describes how to use UI Kit and Forge storage to extend a custom merge check app.
In Part 1, we made sure the title of a PR did not include the string "DRAFT". Alternatively, you might have scenarios where you want to ensure the title does include some particular piece of text. For example, you might have a process where every pull request needs to include a specific prefix from a list of options, or match a regex pattern.
In Part 2, we're going to modify the merge check we made in Part 1 such that it can prevent a pull request from being merged if its title includes a specific substring set by a repository admin. You can view the complete app code in the Bitbucket pull request title validator repository.
This is part 2 of 2 in this tutorial. Complete Part 1: Build a pull request title validator with custom merge checks before going through this page.
We will be adding more resources and functions to the app. Let's refactor it so that it is easier to extend.
Navigate to the app's top-level directory.
Create a src/merge-checks
directory and move src/index.js
to src/merge-checks/index.js
.
Open src/merge-checks/index.js
and rename run
to checkPullRequest
.
Create a src/index.js
and export the checkPullRequest
function.
1 2import { checkPullRequest } from "./merge-checks"; export { checkPullRequest };
Your app should have the following structure:
1 2pr-title-validator ├── README.md ├── manifest.yml ├── package-lock.json ├── package.json └── src ├── index.js └── merge-checks └── index.js
The manifest.yml
file will also need to be updated to reflect the structural changes made to the app.
function
under bitbucket:mergeCheck
to check-pull-request.description
under bitbucket:mergeCheck
to Check pull request title contains a configured substring.key
under function
to check-pull-request.handler
under function
to index.checkPullRequest.Your manifest.yml
should look like the following:
1 2permissions: scopes: - read:pullrequest:bitbucket modules: bitbucket:mergeCheck: - key: check-pr-title function: check-pull-request name: Check pull request title description: Check pull request title contains a configured substring triggers: - on-merge function: - key: check-pull-request handler: index.checkPullRequest app: runtime: name: nodejs18.x id: '<your-app-id>'
Test that the app is still behaving as expected after the refactor.
Deploy your app by running:
1 2forge deploy
You do not need to upgrade the app's installation as app permissions have not changed.
Test your app as described in Part 1: View your app
We need a way to store and retrieve the string we want to check for when validating the pull request title. To do this, define resolver methods for the frontend to call. The resolver methods will interact with Forge's storage API for storing and retrieving the configured string.
In the app's top-level directory, install the @forge/resolver
package by running:
1 2npm install @forge/resolver
Create a src/resolvers
directory and a src/resolvers/index.js
file to contain the resolver functions that will interact with Forge storage.
Your app should have the following structure:
1 2pr-title-validator ├── README.md ├── manifest.yml ├── package-lock.json ├── package.json └── src ├── index.js ├── merge-checks │ └── index.js └── resolvers └── index.js
Open src/resolvers/index.js
and add helper methods to store and retrieve the configured pull request title.
1 2import Resolver from "@forge/resolver"; import { storage } from "@forge/api"; const resolver = new Resolver(); resolver.define("getTitleSubstring", async (request) => { const titleSubstring = await storage.get("titleSubstring") || ""; return titleSubstring; }); resolver.define("updateTitleSubstring", async (request) => { await storage.set("titleSubstring", request.payload.titleSubstring); }); export const resolverHandler = resolver.getDefinitions();
Open src/index.js
and export the resolverHandler
from src/resolvers/index.js
.
1 2import { resolverHandler } from "./resolvers"; export { resolverHandler };
We render a form on the repository settings page for repository admins to configure a string they expect to appear in the title of pull requests in the repository. The UI will invoke our resolver methods from the earlier section using Forge bridge APIs.
In the app's top-level directory, install the react
, @forge/react
and @forge/bridge
packages by running:
1 2npm install react @forge/react @forge/bridge
Create a src/frontend
directory and a src/frontend/index.jsx
file to contain the frontend code.
Your app should have the following structure:
1 2pr-title-validator ├── README.md ├── manifest.yml ├── package-lock.json ├── package.json └── src ├── index.js ├── frontend │ └── index.jsx ├── merge-checks │ └── index.js └── resolvers └── index.js
Open src/frontend/index.js
and create a TitleSubstringForm
component that will invoke the resolver method to store the substring when the form is submitted.
1 2import { invoke, showFlag } from "@forge/bridge"; import { ErrorMessage, Form, FormFooter, Label, LoadingButton, RequiredAsterisk, Textfield, useForm, } from "@forge/react"; const TitleSubstringForm = ({ defaultValues }) => { const { handleSubmit, register, getFieldId, formState } = useForm({ defaultValues, }); const { errors, isSubmitting } = formState; const onSubmit = async (data) => { if (data.titleSubstring) { await invoke("updateTitleSubstring", data); showFlag({ id: "flag", title: "Changed title substring", type: "success", appearance: "success", description: `Set title substring to: ${data.titleSubstring}`, isAutoDismiss: true, }); } }; return ( <Form onSubmit={handleSubmit(onSubmit)}> <Label labelFor={getFieldId("titleSubstring")}> Pull request title substring <RequiredAsterisk /> </Label> <Textfield {...register("titleSubstring", { required: true })} /> {errors.titleSubstring && ( <ErrorMessage>This field is required</ErrorMessage> )} <FormFooter align="start"> <LoadingButton type="submit" isLoading={isSubmitting}> Submit </LoadingButton> </FormFooter> </Form> ); };
Add an App
component that will invoke the resolver method to retrieve the stored substring
and pass it as a defaultValue
to the TitleSubstringForm
component.
1 2import { useEffect, useState } from "react"; import { Inline, Spinner, Text } from "@forge/react"; const App = () => { const [titleSubstring, setTitleSubstring] = useState(); useEffect(() => { invoke("getTitleSubstring").then((substring) => { setTitleSubstring(substring); }); }, []); return ( <> {titleSubstring === undefined ? ( <Inline alignInline="center"> <Spinner size="large" /> </Inline> ) : ( <> <Text>Configure the required string in pull request title</Text> <TitleSubstringForm defaultValues={{ titleSubstring }} /> </> )} </> ); };
Render the App
component using the ForgeReconciler
.
1 2import ForgeReconciler from "@forge/react"; import React from "react"; ForgeReconciler.render( <React.StrictMode> <App /> </React.StrictMode> );
Your src/frontend/index.jsx
should look something like this:
1 2import { invoke, showFlag } from "@forge/bridge"; import ForgeReconciler, { ErrorMessage, Form, FormFooter, Inline, Label, LoadingButton, RequiredAsterisk, Spinner, Text, Textfield, useForm, } from "@forge/react"; import React, { useEffect, useState } from "react"; const TitleSubstringForm = ({ defaultValues }) => { const { handleSubmit, register, getFieldId, formState } = useForm({ defaultValues, }); const { errors, isSubmitting } = formState; const onSubmit = async (data) => { if (data.titleSubstring) { await invoke("updateTitleSubstring", data); showFlag({ id: "flag", title: "Changed title substring", type: "success", appearance: "success", description: `Set title substring to: ${data.titleSubstring}`, isAutoDismiss: true, }); } }; return ( <Form onSubmit={handleSubmit(onSubmit)}> <Label labelFor={getFieldId("titleSubstring")}> Pull request title substring <RequiredAsterisk /> </Label> <Textfield {...register("titleSubstring", { required: true })} /> {errors.titleSubstring && ( <ErrorMessage>This field is required</ErrorMessage> )} <FormFooter align="start"> <LoadingButton type="submit" isLoading={isSubmitting}> Submit </LoadingButton> </FormFooter> </Form> ); }; const App = () => { const [titleSubstring, setTitleSubstring] = useState(); useEffect(() => { invoke("getTitleSubstring").then((substring) => { setTitleSubstring(substring); }); }, []); return ( <> {titleSubstring === undefined ? ( <Inline alignInline="center"> <Spinner size="large" /> </Inline> ) : ( <> <Text>Configure the required string in pull request title</Text> <TitleSubstringForm defaultValues={{ titleSubstring }} /> </> )} </> ); }; ForgeReconciler.render( <React.StrictMode> <App /> </React.StrictMode> );
Update the merge check to retrieve the stored substring and return a response payload based on whether the pull request title contains the substring.
Open src/merge-checks/index.js
.
Import storage
from @forge/api
so that configured substring can be retrieved.
1 2import { storage } from "@forge/api";
In the same file, before you call the Bitbucket REST API, retrieve the configured pull request title substring from Forge Storage. If the substring is not configured, return a failure response payload.
1 2const substring = await storage.get("titleSubstring"); if (!substring) { console.log("Title substring is not configured. Failing merge check."); return { success: false, message: "Title substring is not configured", }; }
Modify the check criteria to be successful if the pull request contains the substring.
1 2const success = pr.title.includes(substring); const message = success ? `Pull request title contains ${substring}` : `Pull request title does not contain ${substring}`;
Your src/merge-checks/index.js
should look like the following:
1 2import api, { route, storage } from "@forge/api"; export const checkPullRequest = async (event, context) => { const workspaceUuid = event.workspace.uuid; const repoUuid = event.repository.uuid; const prId = event.pullrequest.id; const substring = await storage.get("titleSubstring"); if (!substring) { console.log("Title substring is not configured. Failing merge check."); return { success: false, message: "Title substring is not configured", }; } const res = await api .asApp() .requestBitbucket( route`/2.0/repositories/${workspaceUuid}/${repoUuid}/pullrequests/${prId}` ); const pr = await res.json(); const success = pr.title.includes(substring); const message = success ? `Pull request title contains ${substring}` : `Pull request title does not contain ${substring}`; return { success, message }; };
In the app's top-level directory, open the manifest.yml
file.
Add the storage:app
permission to the scopes
under permissions
. This scope is required for Forge Storage.
Add a bitbucket:repoSettingsMenuPage
module entry
for the repository settings page we added.
1 2bitbucket:repoSettingsMenuPage: - key: settings-page resource: main render: native title: Check pull request title resolver: function: resolver
Add a new function
module entry for the repository settings page resolver.
1 2function: - key: resolver handler: index.resolverHandler
Add a new resources
entry for the repository settings page resource.
1 2resources: - key: main path: src/frontend/index.jsx
Your manifest.yml
should look like the following:
1 2permissions: scopes: - read:pullrequest:bitbucket - storage:app modules: bitbucket:mergeCheck: - key: check-pr-title function: check-pull-request name: Check pull request title description: Check pull request title contains the string configured in settings triggers: - on-merge bitbucket:repoSettingsMenuPage: - key: settings-page resource: main render: native title: Check pull request title resolver: function: resolver function: - key: check-pull-request handler: index.checkPullRequest - key: resolver handler: index.resolverHandler resources: - key: main path: src/frontend/index.jsx app: runtime: name: nodejs18.x id: '<your-app-id>'
Deploy your app by running:
1 2forge deploy
Upgrade the app's installation. This is necessary as the app's permissions have changed.
1 2forge install --upgrade
Navigate to your repository settings and open the "Check pull request title" menu item under the FORGE APPS
section.
Set the pull request title substring value. For example, TICKET-.
Ensure the merge check is enabled via the Repository settings → Custom merge checks page if it is not already enabled.
Create a pull request without the substring in the title.
Merge the pull request. The merge check should fail.
Create another pull request with the substring in the title.
Merge the pull request. This time the merge check should pass.
Check out an example app, continue to one of the other tutorials, or read through the reference pages to learn more.
Rate this page: