Update Linear after successful deploys
Linear + GitHub packaged integration
Linear is a really great tool for managing your software development process (we use it for Lightyear!). Of course it comes with a GitHub integration, which can automatically move issues between stages when the related code is merged. From the linear docs (opens in a new tab):
We'll move the issue to In Progress when the branch is pushed and Done when the commit is merged to the default branch.
This works great when you are just starting out.
But when you need to deploy the code to staging and production, you quickly notice that the issue state is updated before the code is deployed and it remains in that state whether the deploy succeeds or fails.
If your team is in this situation and would like to solve this problem, read on!
Update the stages after deployment
The solution to this problem can be broken down into 4 parts
- Determine when a deployment is successful
- Determine which commits were just deployed
- Determine which Linear issues are referenced in the commit messages
- Update the stage of the Linear issues
Determine when a deployment is successful
If you are using GitHub actions, one easy way to do this is to listen for workflow_run
events and check to see if they are completed and successful. To build this, first create a workflow run listener:
import { GitHub } from "@runlightyear/github";
GitHub.onWorkflowRun({
name: "updateLinearAfterDeploy",
title: "Update Linear After Deploy",
owner: "<owner>",
repo: "<repo>",
run: async ({ data }) => {
// logic goes here
},
});
Now that we have an action running on every workflow_run
event on this repo, we can inspect the data to see if the event we just received represents a completed run.
import { GitHub } from "@runlightyear/github";
GitHub.onWorkflowRun({
name: "updateLinearAfterDeploy",
title: "Update Linear After Deploy",
owner: "ebouck",
repo: "test-repo",
run: async ({ data }) => {
if (data.action === "completed") {
// logic goes here
}
},
});
Next we need to know if it was successful.
import { GitHub } from "@runlightyear/github";
GitHub.onWorkflowRun({
name: "updateLinearAfterDeploy",
title: "Update Linear After Deploy",
owner: "ebouck",
repo: "test-repo",
run: async ({ data }) => {
if (data.action === "completed") {
if (data.workflowRun.conclusion === "success") {
// logic goes here
}
}
},
});
Finally, we need to know if this was on a branch that we care about. For this guide, we'll just consider pushing to main
.
import { GitHub } from "@runlightyear/github";
GitHub.onWorkflowRun({
name: "updateLinearAfterDeploy",
title: "Update Linear After Deploy",
owner: "ebouck",
repo: "test-repo",
run: async ({ data }) => {
if (data.action === "completed") {
if (data.workflowRun.conclusion === "success") {
if (data.workflowRun.headBranch === "main") {
// logic goes here
}
}
}
},
});
Now we can decide what to do with successful deploys to main
.
Determine which commits were just deployed
We can use the GitHub API to compare the current head commit with the most recent successfully deployed commit to determine the commits were just deployed. How do we get the most recent successfully deployed commit? We store it in a variable after each successful run. Let's create a variable called lastSuccessfulCommitId
and store the head commit id on it each time we get a successful run.
import { GitHub } from "@runlightyear/github";
import { setVariable } from "@runlightyear/lightyear";
GitHub.onWorkflowRun({
name: "updateLinearAfterDeploy",
title: "Update Linear After Deploy",
owner: "ebouck",
repo: "test-repo",
variables: ["lastSuccessfulCommitId?"],
run: async ({ data }) => {
if (data.action === "completed") {
if (data.workflowRun.conclusion === "success") {
if (data.workflowRun.headBranch === "main") {
const headCommitId = data.workflowRun.headCommit.id;
// logic goes here
await setVariable("lastSuccessfulCommitId", headCommitId);
}
}
}
},
});
You might be wondering why there's a ?
in the variable declaration. This is to denote that the variable is optional and that the action is runnable even though the variable is not set.
Next we need to call the GitHub api to compare the head commit with the last successful commit.
import { GitHub } from "@runlightyear/github";
import { setVariable } from "@runlightyear/lightyear";
GitHub.onWorkflowRun({
name: "updateLinearAfterDeploy",
title: "Update Linear After Deploy",
owner: "ebouck",
repo: "test-repo",
variables: ["lastSuccessfulCommit?"],
run: async ({ data, auths, variables }) => {
if (data.action === "completed") {
if (data.workflowRun.conclusion === "success") {
if (data.workflowRun.headBranch === "main") {
const headCommitId = data.workflowRun.headCommit.id;
const github = new GitHub({ auth: auths.github });
const githubResponse = await github.compareTwoCommits({
owner: "ebouck",
repo: "test-repo",
basehead: `${variables.lastSuccessfulCommitId}...${headCommitId}`,
});
const { commits } = githubResponse.data;
// logic goes here
await setVariable("lastSuccessfulCommit", headCommitId);
}
}
}
},
});
But what if the lastSuccessfulCommit variable isn't set yet? We can supply the headCommitId as a default, which will give us an empty commit list. Better than an error!
import { GitHub } from "@runlightyear/github";
import { setVariable } from "@runlightyear/lightyear";
GitHub.onWorkflowRun({
name: "updateLinearAfterDeploy",
title: "Update Linear After Deploy",
owner: "ebouck",
repo: "test-repo",
variables: ["lastSuccessfulCommitId?"],
run: async ({ data, auths, variables }) => {
if (data.action === "completed") {
if (data.workflowRun.conclusion === "success") {
if (data.workflowRun.headBranch === "main") {
const headCommitId = data.workflowRun.headCommit.id;
const github = new GitHub({ auth: auths.github });
const githubResponse = await github.compareTwoCommits({
owner: "ebouck",
repo: "test-repo",
basehead: `${
variables.lastSuccessfulCommitId || headCommitId
}...${headCommitId}`,
});
const { commits } = githubResponse.data;
// logic goes here
await setVariable("lastSuccessfulCommitId", headCommitId);
}
}
}
},
});
When all this is done, we have a variable called commits
that contains all the commits since the last successful deploy.
Determine which Linear issues are referenced in the commit messages
To keep our code clean, let's put the logic for finding Linear identifiers in a function.
import { GitHub, Commit } from "@runlightyear/github";
import { setVariable } from "@runlightyear/lightyear";
function getLinearIdentifiers(commits: Array<Commit>) {
// logic goes here
}
...
We will need to define a regex to match the linear identifiers.
Make sure that the prefix in the regex matches in your Linear instance. We are assuming ENG in this guide.
import { GitHub, Commit } from "@runlightyear/github";
import { setVariable } from "@runlightyear/lightyear";
function getLinearIdentifiers(commits: Array<Commit>) {
const identifierRegex = /ENG-[0-9]+/g;
// logic goes here
}
Now let's get the list of matching identifiers by applying the regex to each commit message and then flattening the result.
import { GitHub, Commit } from "@runlightyear/github";
import { setVariable } from "@runlightyear/lightyear";
function getLinearIdentifiers(commits: Array<Commit>) {
const identifierRegex = /ENG-[0-9]+/g;
const matches = commits
.map((commit) => commit.commit.message.matchAll(identifierRegex))
.flat();
// logic goes here
}
...
And to be safe, let's return a de-duped list of matching identifiers by using a set.
import { GitHub, Commit } from "@runlightyear/github";
import { setVariable } from "@runlightyear/lightyear";
function getLinearIdentifiers(commits: Array<Commit>) {
const identifierRegex = /ENG-[0-9]+/g;
const matches = commits
.map((commit) => commit.commit.message.matchAll(identifierRegex))
.flat();
return new Set(matches);
}
...
Now let's add the call to this function to our main function.
import { GitHub, Commit } from "@runlightyear/github";
import { setVariable } from "@runlightyear/lightyear";
function getLinearIdentifiers(commits: Array<Commit>) {
const identifierRegex = /ENG-[0-9]+/g;
const matches = commits
.map((commit) => commit.commit.message.matchAll(identifierRegex))
.flat();
return new Set(matches);
}
GitHub.onWorkflowRun({
name: "updateLinearAfterDeploy",
title: "Update Linear After Deploy",
owner: "ebouck",
repo: "test-repo",
variables: ["lastSuccessfulCommitId?"],
run: async ({ data, auths, variables }) => {
if (data.action === "completed") {
if (data.workflowRun.conclusion === "success") {
if (data.workflowRun.headBranch === "main") {
const headCommitId = data.workflowRun.headCommit.id;
const github = new GitHub({ auth: auths.github });
const githubResponse = await github.compareTwoCommits({
owner: "ebouck",
repo: "test-repo",
basehead: `${
variables.lastSuccessfulCommitId || headCommitId
}...${headCommitId}`,
});
const { commits } = githubResponse.data;
const identifiers = getLinearIdentifiers(commits);
// logic goes here
await setVariable("lastSuccessfulCommitId", headCommitId);
}
}
}
},
});
Update the stage of the Linear issues
First, let's add the Linear app and create a connector to it.
...
GitHub.onWorkflowRun({
name: "updateLinearAfterDeploy",
title: "Update Linear After Deploy",
owner: "ebouck",
repo: "test-repo",
apps: ["linear"],
variables: ["lastSuccessfulCommitId?"],
run: async ({ data, auths, variables }) => {
if (data.action === "completed") {
if (data.workflowRun.conclusion === "success") {
if (data.workflowRun.headBranch === "main") {
const headCommitId = data.workflowRun.headCommit.id;
const github = new GitHub({ auth: auths.github });
const githubResponse = await github.compareTwoCommits({
owner: "ebouck",
repo: "test-repo",
basehead: `${
variables.lastSuccessfulCommitId || headCommitId
}...${headCommitId}`,
});
const { commits } = githubResponse.data;
const identifiers = getLinearIdentifiers(commits);
const linear = new Linear({ auth: auths.linear });
// logic goes here
}
await setVariable("lastSuccessfulCommitId", headCommitId);
}
}
},
});