Starting point
At Wecasa, on the frontend side, we have 2 mobile apps that we are maintaining regularly, delivering new versions on both Android and iOS stores. Multiple teams with different scopes are working on these apps, and it's the responsibility of each team to deploy their work.
When one or more features are ready to be shipped in production, one dev has to prepare the new build. Let's dig into the different steps the developer has to do:
- Merge commits that are going to prod on master
- From the monorepo root, run a bash script (one bash script per app)
!! All our front code is located in one monorepo. Severals projects are living inside, including our 2 mobiles apps. Until now we had two separate bash scripts to deploy either one or the other app
- The script defines the new app version number
- Then it updates build.gradle and info.plist with the new version number
- It creates a new tag on GitHub that will trigger a workflow on our CI. This workflow will create the actual builds and send them to the stores
And that's pretty much it.
Job done ✅
Improve the experience
Recently, we decided to improve our deployment process. But why? The old process was just working fine, right?
Well, yes, it got the job done, but a few things were missing:
- Until now, we didn't have any changelog for any release. So sometimes, when you wanted to know which commit had been introduced in which specific version, it was not so easy to find out.
- The version number generated by the old bash script was a patch release by default. We had to force a version number if we wanted to do a minor or major one. We wanted it to be easier to follow a proper semantic versioning notation.
- It was a bash script. Bash is great, but few people know how to code in Bash, so few people were comfortable understanding the script and, furthermore, making adjustments to it.
A CLI to Rule Them All
To improve the experience of releasing, we decided to make a CLI in JS. The CLI will do all the tasks that the Bash script did and a few more.
We chose to build a CLI because:
- It's interactive and fun to play with as a developer
- It allows some flexibility and different options through the prompts
- It's made in JS, so our tech team will be more comfortable adding updates to it
Inquirer JS
To build this CLI, we used the Inquirer library that allows us to quickly ask questions and store the responses:
import select from "@inquirer/select";
export const askRepo = async () =>
select({
message: "Choose the app you want to deploy",
choices: REPOS.map((repo) => ({
name: repo,
value: repo,
})),
});
Bash to JS
Moving from a bash script to a JS CLI implies that we had to run bash commands in our JS environment. In order to execute bash commands in JS environment, we used the execSync
method from Node a lot.
With this utility function ⬇️ , it was easy to migrate all bash commands to the JS scripts:
import { execSync } from "child_process";
export const executeBashCommand = (command: string) => {
try {
return execSync(command, {
encoding: "utf-8",
cwd: REPO_ROOT,
});
} catch (error) {
console.error("Error executing command:", error);
}
};
Automatize Github release with changelogs
This refactor was also a good opportunity to create a proper release in GitHub with a clean changelog.
To do that, we used the GitHub API to retrieve the information we needed from our repo. More specifically by using Octokit .
Create a tag:
A release in GitHub is coupled to a tag
. You can't have a release if it is not associated with a tag. So the first step was to create that tag:
- We want the tag to reflect the type of release we want to make, so in our CLI, we now ask what kind of release we want: patch, minor or major.
- Once the type of release known, we generate the version number.
- Once we have the version number, we build our tag name and create it on GitHub.
// Generate a string based on the app we want to deploy, the next version number and the stores
let newTag = getNewTag({ app, stores, version: nextVersion });
// We check if this new generated tag already exists in Github before continuing
if (await isTagAlreadyExists(newTag)) {
console.log(
`⚠️ This tag already exists on Github, which means release ${app}-v${nextVersion} probably already exists too`
);
console.log(
"Maybe this tag was published by mistake and no build has been sent to any store. You have two options:"
);
console.log(
"1. You are sure that this release on Github is a mistake so you can delete it with the associated tag"
);
console.log(
`2. You can choose another release number, different from ${nextVersion}`
);
const whatToDo = await askIfCustomVersion();
if (whatToDo === "exit") {
console.log("👋 Okay bye !\n\n");
process.exit(0);
}
if (whatToDo === "change") {
nextVersion = await nlThenInput({
message: "Type the desired version number",
});
newTag = getNewTag({ app, stores, version: nextVersion });
}
}
As you can see, we will also verify if this tag already exists in our repo and do specific actions in this case.
Generate release content and publish:
Now that we have a tag name, all we need to do is create a release with its appropriate changelog.
To build this changelog, we:
- Get the latest release date
- Fetch all the commits merged to master since this date
- Create the body of the changelog, grabbing each commit message and its author
- Once we have the content, the final step is to push the release to GitHub:
// #1 Get the latest release date
export const getLatestReleases = async (): Promise<Release[]> => {
const releases = await octokit.rest.repos.listReleases({
owner: REPO_OWNER,
repo: REPO,
per_page: 100,
});
return releases.data.sort(
(a, b) => new Date(b.published_at) - new Date(a.published_at)
);
};
// #2 Fetch all the commits merged to master since this date
export const getMergedCommitsFromDate = async (
date?: string
): Promise<Commit[]> => {
if (!date) return [];
const commits = await octokit.repos.listCommits({
owner: REPO_OWNER,
repo: REPO,
sha: "master",
since: date,
per_page: 100,
});
return commits.data;
};
// #3 Create the body of the changelog, grabbing each commit message and its author
export const getReleaseContent = (commits: Commit[], stores: AppStore[]) => {
const mdFormattedCommits = commits.map(commitMarkdowned);
return `## What is included?\n${mdFormattedCommits.join(
"\n"
)}\n\n## Stores:\n${stores.join(" and ")}`;
};
// #4 Push the release to Github
export const createRelease = async ({
tagName,
releaseName,
body,
}: CreateReleaseProps) => {
const release = await octokit.rest.repos.createRelease({
owner: REPO_OWNER,
repo: REPO,
tag_name: tagName,
name: releaseName,
body,
draft: false,
make_latest: "true",
});
return release;
};
And it’s done 🎉 Below, you can have a quick preview of what the experience looks like in terminal for a dev who wants to release:
Conclusion
To summarize, we changed from an entire bash script that was quite intimidating for devs with no experience in bash language, to a CLI made in JavaScript, so very close to our stack. And as a result, easier to read, understand, and maintain.
We grabbed this opportunity to also create proper releases in our GitHub with changelogs, which will bring tech and product teams more transparency through a clean feature history.
New | Old | |
---|---|---|
Code | TypeScript | Bash |
One script for both app | ✅ | ❌ |
Changelogs in Git | ✅ | ❌ |
Choice of version | ✅ | ❌ |
CLI | ✅ | ❌ |
Maintainability | Easier in JS | Harder because it’s bash |
Thanks for reading!
Top comments (0)