Skip to content

Commit

Permalink
chore(ci): improve release production flow
Browse files Browse the repository at this point in the history
  • Loading branch information
yeager-eren authored and nikaaru committed Jan 10, 2024
1 parent 5517c14 commit 3d1c260
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 129 deletions.
3 changes: 3 additions & 0 deletions .github/PUBLISH_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Checklist

TBA
10 changes: 8 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ on:
push:
branches:
- main
tags:
- '!*'
pull_request:
types:
- closed
Expand Down Expand Up @@ -43,3 +41,11 @@ jobs:
REF: ${{ github.ref }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
GH_TOKEN: ${{ github.token }}

- name: Creating PR on next
if: ${{ github.ref == 'refs/heads/main' }}
run: yarn run post-release-prod
env:
REF: ${{ github.ref }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
GH_TOKEN: ${{ github.token }}
48 changes: 1 addition & 47 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,52 +29,6 @@ Here is the structure:
`/widget/playground`: This directory offers a playground environment where you can test and obtain configurations for our widget.
`/widget/iframe`: This directory contains a JavaScript class that simplifies the process of adding our iframe-based widget to dApps.


## Release workflow

A release can be a lib or an app/client release. We are publishing our libs to `npm` and deploying our apps (client) on `vercel`.

If a package is app, you need to add the package name to `scripts/deploy/config.mjs` and then after getting a `PROJECT_ID` from Vercel, you need to set it as enviroment variable as well.

There are main commands:

`yarn run publish` for publishing our NPM packages.
`yarn run deploy` to deploy apps on Vercel.

### Publish

#### Prerelase

Our publish script will do these steps:

1. Get the last release (by using git tags) and calculate changes since then.
2. Bump the version for changed packages.
3. Create changelog, git tags and github release, and publish them to NPM.
4. Make a publish commit and push the updated packages (version) and tags to origin.

Note:
Libs will be published under `next` tag on npm, which means you need to use `yarn add @rango/test-package@next` to install the published version whenever you need.

#### Production relase

Release should be triggered manually and then it will automatically published. You only need to run this command on you local machine to release the production:

`yarn run release-prod`

After release (Green pipleline), make sure you will merge `main` into `next` as well.

`yarn run post-release-prod`

### Deploy

You should manually trigger the `deploy` workflow.

By running `yarn run deploy`, it will build all the apps/clients then will try to deploy them on vercel.

If the workflow is running on `next` branch, it will be deployed as Vercel's `preview`. If not, it's production release.

All the apps published by `prerelase` workflow will be published under the Vercel's `preview` enviroment.

## Translation

First we need to extract the message from our source code using `yarn i18n:extract` and then we should run `yarn i18n:compile` to make a wrapper arround the translation file `.po` to be used inside our app.
Expand Down Expand Up @@ -115,7 +69,7 @@ Additionally, there is the option to manually trigger the crowdin workflow if ne

### How we handle urls in embedded?

`embedded` is importing in different enviroments (iframe, import as react component and a separate `app`). When it's being used as a react component we let the dApp to use it inside its router and they can load the widget in a separate route (not root `/`, e.g. `/swaps`). In this context we can not use absolute paths because we don't know the basename set by user, so we need to always use relative paths. Relative paths has been defined in [URL spec](https://url.spec.whatwg.org/#urls) so we can be sure it's a long term solution and will work in future or by changing our router librart (e.g. `react-router`).
`embedded` is importing in different environments (iframe, import as react component and a separate `app`). When it's being used as a react component we let the dApp to use it inside its router and they can load the widget in a separate route (not root `/`, e.g. `/swaps`). In this context we can not use absolute paths because we don't know the basename set by user, so we need to always use relative paths. Relative paths has been defined in [URL spec](https://url.spec.whatwg.org/#urls) so we can be sure it's a long term solution and will work in future or by changing our router librart (e.g. `react-router`).

### HMR

Expand Down
18 changes: 18 additions & 0 deletions docs/fork.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Fork

## How you can test release flow?

You need to add your [Github Token](https://github.com/settings/tokens) and set it as `PAT` in your fork's secrets.

for NPM:

- Create an NPM account and an organization. (I created @yeager-dev for example)
- Get a token from NPM and set it as `NPM_TOKEN` in our repo.
- Enable Github Actions on your fork.

_Note 1_: If you are a Rango developer, you can ask NPM token for `@yeager-dev` org.

For Crowdin:

After creating a Crowdin project, it has an ID (you can find it in right sidebar) and for accessing to API you will [need a token](https://crowdin.com/settings#api-key) as well.
Then, you need to set `CROWDIN_PERSONAL_TOKEN` and `CROWDIN_PROJECT_ID` in your secrets.
3 changes: 3 additions & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Docs

Here, we are documenting some of our internal processes.
60 changes: 60 additions & 0 deletions docs/release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Release

## How we are releasing a new version?

A release can be a lib or an app/client release. We are publishing our libs to `npm` and deploying our apps (client) on `vercel`.

If a package is app, you need to add the package name to `scripts/deploy/config.mjs` and then after getting a `PROJECT_ID` from Vercel, you need to set it as enviroment variable as well.

There are main commands:

`yarn run publish` for publishing our NPM packages.
`yarn run deploy` to deploy apps on Vercel.

### Publish flow

#### Prerelase

Our publish script will do these steps:

1. Get the last release (by using git tags) and calculate changes since then.
2. Bump the version for changed packages.
3. Create changelog, git tags and github release, and publish them to NPM.
4. Make a publish commit and push the updated packages (version) and tags to origin.

Note:
Libs will be published under `next` tag on npm, which means you need to use `yarn add @rango/test-package@next` to install the published version whenever you need.

#### Production relase

Release should be triggered manually and then it will automatically published. You only need to run this command on you local machine to release the production:

`yarn run release-prod`

### Deploy flow

You should manually trigger the `deploy` workflow.

By running `yarn run deploy`, it will build all the apps/clients then will try to deploy them on vercel.

If the workflow is running on `next` branch, it will be deployed as Vercel's `preview`. If not, it's production release.

All the apps published by `prerelase` workflow will be published under the Vercel's `preview` enviroment.

## How you can release a new version?

### Next (Staging)

A publish only will be triggered when a **Pull Request** has been merged. If you try to commit directly into the `next` branch it wouldn't be triggered.

First it tries to extracting translations (if any) and push them onto Crowdin, then releasing libraries will be started.
_Note 1_: Syncing translations (first workflow) is an optional step which means if it fails we will do the publish anyway.

### Production

For releasing production, you need to run `yarn release-prod` it will checkout to `next` branch and pull the latest changes then it tries to merge the `next` into `main`.
_Note 1_: Make sure you are having permission for `push` on `main`.

In production, we don't run localization workflow (crowdin) since we assume our `/translation` folder is in sync with Crowdin. if you think there is new translation, you can run `crowdin` workflow manually and then try to release.

At the end, a PR will be created to merge `main` into `next` after publishing the libraries. You need to check the PR description and make sure you are considering/doing them.
1 change: 0 additions & 1 deletion scripts/common/constants.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
/** A fixed subject for using in publish commit. */
export const PUBLISH_COMMIT_SUBJECT = 'chore(release): publish';
export const NPM_ORG_NAME = '@rango-dev';
7 changes: 7 additions & 0 deletions scripts/common/errors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ export class GithubCreateReleaseFailedError extends Error {
}
}

export class GithubCommandError extends Error {
name = 'GithubCommandError';
constructor(msg) {
super(msg);
}
}

export class NpmPackageNotFoundError extends Error {
name = 'NpmPackageNotFoundError';
constructor(packageName) {
Expand Down
46 changes: 30 additions & 16 deletions scripts/common/git.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -147,21 +147,26 @@ export async function getChangedPackagesFor(channel) {
*
*/
export async function publishCommitAndTags(pkgs) {
const isTaggingSkipped = detectChannel() !== 'prod';
const channel = detectChannel();
const isTaggingSkipped = channel !== 'prod';
const subject = `${PUBLISH_COMMIT_SUBJECT}\n\n`;
const tags = pkgs.map(generateTagName);

const list = tags.map((tag) => `- ${tag}`).join('\n');
const message = subject + list;
let body = `Affected packages: ${tags.join(',')}`;

/*
When we are pushing a publish commit into main, it triggers a redundant workflow run,
To avoid this, by adding a [skip ci] the workflow run will be skipped.
We don't need it on `next` since the next workflow is running on `pullrequest.closed` event.
*/
if (channel === 'prod') {
body += '\n[skip ci]';
}

// Making a publish commit
await execa('git', [
'commit',
'-m',
message,
'-m',
`Affected packages: ${tags.join(',')}`,
]).catch((error) => {
await execa('git', ['commit', '-m', message, '-m', body]).catch((error) => {
throw new GitError(`git commit failed. \n ${error.stderr}`);
});

Expand Down Expand Up @@ -194,14 +199,23 @@ export async function addFileToStage(path) {
});
}

export async function push(remote = 'origin') {
const output = await execa('git', [
'push',
remote,
'--follow-tags',
'--no-verify',
'--atomic',
])
export async function push(options) {
const { setupRemote, branch, remote = 'origin' } = options || {};

let pushOptions = [];
if (setupRemote) {
if (!branch) {
throw new CustomScriptError(
`You should also pass branch name as parameter to push. \n ${error.stderr}`
);
}

pushOptions = ['--set-upstream', remote, branch];
} else {
pushOptions = [remote, '--follow-tags', '--no-verify', '--atomic'];
}

const output = await execa('git', ['push', ...pushOptions])
.then(({ stdout }) => stdout)
.catch((error) => {
throw new GitError(`git push failed. \n ${error.stderr}`);
Expand Down
43 changes: 43 additions & 0 deletions scripts/common/github.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { execa } from 'execa';
import { generateChangelog } from './changelog.mjs';
import {
GithubCommandError,
GithubCreateReleaseFailedError,
GithubGetReleaseError,
GithubReleaseNotFoundError,
Expand Down Expand Up @@ -82,6 +83,48 @@ export async function githubReleaseFor(pkg) {
}
}

/**
*
* @param {PullRequestInfo} pr
*
* @typedef {Object} PullRequestInfo
* @property {string} title PR title
* @property {string} branch your current branch
* @property {string} baseBranch PR will be merge into base branch.
* @property {string} templatePath template path for PR
*
*/
export async function createPullRequest(pr) {
const { title, baseBranch, branch, templatePath } = pr;

if (!title || !baseBranch || !branch || !templatePath) {
throw new GithubCommandError(
'Creating pull request can not be proceed without required parameters. \n',
JSON.stringify({ title, baseBranch, branch, templatePath })
);
}

const ghCreateParams = [
'--title',
title,
'--base',
baseBranch,
'--head',
branch,
'--body-file',
templatePath,
];
const output = await execa('gh', ['pr', 'create', ...ghCreateParams])
.then(({ stdout }) => stdout)
.catch((err) => {
throw new GithubCommandError(
`gh pr command failed. \n ${err.stdout || err} \n`
);
});

return output;
}

export function checkEnvironments() {
const envs = {
NPM_TOKEN: !!process.env.NPM_TOKEN,
Expand Down
11 changes: 0 additions & 11 deletions scripts/common/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { readFileSync } from 'fs';
import { join } from 'path';
import { execa } from 'execa';
import process from 'node:process';
import { NPM_ORG_NAME } from './constants.mjs';

const root = join(printDirname(), '..', '..');

Expand Down Expand Up @@ -92,13 +91,3 @@ export function getEnvWithFallback(name) {
export function generateTagName(pkg) {
return `${packageNameWithoutScope(pkg.name)}@${pkg.version}`;
}

/**
* Opposite of `generateTagName`
*
* @param {string} pkgNameWithoutScope
* @returns
*/
export function tagNameToPkgName(pkgNameWithoutScope) {
return `${NPM_ORG_NAME}/${pkgNameWithoutScope}`;
}
17 changes: 8 additions & 9 deletions scripts/post-release/command.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { checkout, merge, pull, push } from '../common/git.mjs';
import { checkCommitAndGetPkgs } from './tag.mjs';
import { checkout, pull } from '../common/git.mjs';
import { createPullRequest } from '../common/github.mjs';

async function run() {
// Make sure we are on main and having latest changes
await checkout('main');
await pull();

await checkCommitAndGetPkgs();

// Merge phase
await checkout('next');
await pull();
await merge('main');
await push();
await createPullRequest({
title: '🤖 Post Release',
branch: 'main',
baseBranch: 'next',
templatePath: '.github/PUBLISH_TEMPLATE.md',
});
}

run().catch((e) => {
Expand Down
Loading

0 comments on commit 3d1c260

Please sign in to comment.