Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: New action to publish releases #29519

Merged
merged 8 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tiny-cups-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/release-action': major
---

New action to publish package releases
68 changes: 68 additions & 0 deletions .github/workflows/new-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Start new release

on:
workflow_dispatch:
inputs:
name:
type: choice
description: Release type
default: next
required: true
options:
- next
- patch
- publish
base-ref:
description: Base version
default: master
required: false

jobs:
new-release:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# with:
# ref: ${{ github.event.inputs.base-ref }}

- name: Setup Node.js 16
uses: actions/setup-node@v3
with:
node-version: 16

- run: yarn install

- name: Build
run: yarn build

- name: Start next release
if: ${{ github.event.inputs.name == 'next' }}
uses: ./packages/release-action
with:
action: bump
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Start patch release
if: ${{ github.event.inputs.name == 'patch' }}
uses: ./packages/release-action
with:
action: patch
base-ref: ${{ github.event.inputs.base-ref }}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Publish release
if: ${{ github.event.inputs.name == 'publish' }}
uses: ./packages/release-action
with:
action: publish
base-ref: ${{ github.event.inputs.base-ref }}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 changes: 35 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Publish Final Release

on:
push:
branches:
- master

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3

- name: Setup Node.js 16
uses: actions/setup-node@v3
with:
node-version: 16

- name: Install Dependencies
run: yarn

- name: Build
run: yarn build

- name: Publish final release
uses: ./packages/release-action
with:
action: publish-final
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 4 additions & 0 deletions packages/release-action/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": ["@rocket.chat/eslint-config"],
"ignorePatterns": ["**/dist"]
}
1 change: 1 addition & 0 deletions packages/release-action/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
1 change: 1 addition & 0 deletions packages/release-action/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @rocket.chat/release-action
1 change: 1 addition & 0 deletions packages/release-action/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# release-action
18 changes: 18 additions & 0 deletions packages/release-action/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Changeset release
description: Action to cut and publish releases using changesets

inputs:
action:
description: "The main action to perform: publish, publish-final, bump or patch"
required: true
base-ref:
description: "Base ref to use for the release"
required: false

runs:
using: "node16"
main: "dist/index.js"

branding:
icon: "package"
color: "blue"
30 changes: 30 additions & 0 deletions packages/release-action/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@rocket.chat/release-action",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "tsc",
"lint": "eslint src",
"lint:fix": "eslint --fix src"
},
"main": "dist/index.js",
"packageManager": "yarn@3.5.1",
"devDependencies": {
"@types/eslint": "^8",
"@types/node": "^16",
"typescript": "^5.1.3"
},
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1",
"@actions/github": "^5.1.1",
"@octokit/plugin-throttling": "^6.0.0",
"@rocket.chat/eslint-config": "workspace:^",
"eslint": "^8.42.0",
"mdast-util-to-string": "2",
"remark-parse": "9",
"remark-stringify": "9",
"semver": "^7.5.1",
"unified": "9"
}
}
92 changes: 92 additions & 0 deletions packages/release-action/src/bumpNextVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import fs from 'fs';
import path from 'path';

import { exec } from '@actions/exec';
import * as core from '@actions/core';
import * as github from '@actions/github';

import { setupOctokit } from './setupOctokit';
import { createNpmFile } from './createNpmFile';
import { getChangelogEntry, updateVersionPackageJson } from './utils';
import { fixWorkspaceVersionsBeforePublish } from './fixWorkspaceVersionsBeforePublish';

export async function bumpNextVersion({
githubToken,
mainPackagePath,
cwd = process.cwd(),
}: {
githubToken: string;
mainPackagePath: string;
cwd?: string;
}) {
const octokit = setupOctokit(githubToken);

// TODO do this only if publishing to npm
await createNpmFile();

// TODO need to check if there is any change to 'main package', if not, there is no need to enter rc
// and instead a normal release of the other packages should be done

// start release candidate
await exec('yarn', ['changeset', 'pre', 'enter', 'rc']);

// bump version of all packages to rc
await exec('yarn', ['changeset', 'version']);

// get version from main package
const mainPackageJsonPath = path.join(mainPackagePath, 'package.json');
// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires
const { version: newVersion } = require(mainPackageJsonPath);

const mainPackageChangelog = path.join(mainPackagePath, 'CHANGELOG.md');

const changelogContents = fs.readFileSync(mainPackageChangelog, 'utf8');
const changelogEntry = getChangelogEntry(changelogContents, newVersion);
if (!changelogEntry) {
// we can find a changelog but not the entry for this version
// if this is true, something has probably gone wrong
throw new Error('Could not find changelog entry for version newVersion');
}

const prBody = changelogEntry.content;

const finalVersion = newVersion.split('-')[0];

const newBranch = `release-${finalVersion}`;

// update root package.json
updateVersionPackageJson(cwd, newVersion);

// TODO check if branch exists
await exec('git', ['checkout', '-b', newBranch]);

await exec('git', ['add', '.']);
await exec('git', ['commit', '-m', newVersion]);

await fixWorkspaceVersionsBeforePublish();

await exec('yarn', ['changeset', 'publish']);

await exec('git', ['push', '--force', '--follow-tags', 'origin', `HEAD:refs/heads/${newBranch}`]);

if (newVersion.includes('rc.0')) {
const finalPrTitle = `Release ${finalVersion}`;

core.info('creating pull request');
await octokit.rest.pulls.create({
base: 'master',
head: newBranch,
title: finalPrTitle,
body: prBody,
...github.context.repo,
});
}

await octokit.rest.repos.createRelease({
name: newVersion,
tag_name: newVersion,
body: prBody,
prerelease: newVersion.includes('-'),
...github.context.repo,
});
}
26 changes: 26 additions & 0 deletions packages/release-action/src/createNpmFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import fs from 'fs';
import fsPromise from 'fs/promises';

import * as core from '@actions/core';

export async function createNpmFile() {
const userNpmrcPath = `${process.env.HOME}/.npmrc`;

if (fs.existsSync(userNpmrcPath)) {
core.info('Found existing user .npmrc file');
const userNpmrcContent = await fsPromise.readFile(userNpmrcPath, 'utf8');
const authLine = userNpmrcContent.split('\n').find((line) => {
// check based on https://github.com/npm/cli/blob/8f8f71e4dd5ee66b3b17888faad5a7bf6c657eed/test/lib/adduser.js#L103-L105
return /^\s*\/\/registry\.npmjs\.org\/:[_-]authToken=/i.test(line);
});
if (authLine) {
core.info('Found existing auth token for the npm registry in the user .npmrc file');
} else {
core.info("Didn't find existing auth token for the npm registry in the user .npmrc file, creating one");
fs.appendFileSync(userNpmrcPath, `\n//registry.npmjs.org/:_authToken=${process.env.NPM_TOKEN}\n`);
}
} else {
core.info('No user .npmrc file found, creating one');
fs.writeFileSync(userNpmrcPath, `//registry.npmjs.org/:_authToken=${process.env.NPM_TOKEN}\n`);
}
}
60 changes: 60 additions & 0 deletions packages/release-action/src/fixWorkspaceVersionsBeforePublish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Changesets doesn't currently support workspace versions:
// https://github.com/changesets/changesets/issues/432
// https://github.com/changesets/action/issues/246
// To work around that, we'll manually resolve any `workspace:` version ranges
// with this tool prior to publishing. If/when changesets adds native support for
// publishing with Yarn 3, we can remove this script.
//
// We'll only support the `workspace:^` range, which is the only one we
// generally want to use.

import fs from 'node:fs/promises';
import path from 'node:path';

import { getExecOutput } from '@actions/exec';

const DEPENDENCY_TYPES = ['dependencies', 'devDependencies', 'peerDependencies'];

export async function fixWorkspaceVersionsBeforePublish() {
const rawWorkspaces = await getExecOutput('yarn workspaces list --json');
const workspaces = rawWorkspaces.stdout
.trim()
.split('\n')
.map((line) => JSON.parse(line))
.filter((workspace) => workspace.location !== '.');

// Get the version of each workspace package.
const workspaceVersions = new Map();
for await (const workspace of workspaces) {
const packageJsonPath = path.join(workspace.location, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
workspaceVersions.set(workspace.name, packageJson.version);
}

// Replace any `workspace:^` version ranges with the actual version.
for await (const workspace of workspaces) {
const packageJsonPath = path.join(workspace.location, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));

for (const dependencyType of DEPENDENCY_TYPES) {
const dependencies = Object.keys(packageJson[dependencyType] ?? {});
for (const dependency of dependencies) {
const dependencyVersion = packageJson[dependencyType][dependency];
if (dependencyVersion.startsWith('workspace:')) {
if (!dependencyVersion.startsWith('workspace:^')) {
throw new Error(`Unsupported workspace version range: ${dependencyVersion}`);
}

const realVersion = workspaceVersions.get(dependency);
if (!realVersion) {
throw new Error(`Could not find version for workspace ${dependency}`);
}

packageJson[dependencyType][dependency] = `^${realVersion}`;
}
}
}

await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
}
}
6 changes: 6 additions & 0 deletions packages/release-action/src/gitUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { exec } from '@actions/exec';

export async function setupGitUser() {
await exec('git', ['config', 'user.name', '"github-actions[bot]"']);
await exec('git', ['config', 'user.email', '"github-actions[bot]@users.noreply.github.com"']);
}
Loading
Loading