Skip to content

Commit

Permalink
chore(ci): push to each repository on PR merge (#257)
Browse files Browse the repository at this point in the history
* chore(ci): push to each repository on PR merge

* chore: separate spreadGeneration workflow

* chore: remove unnecessary eslint disable comment

* chore: rename workflow

* chore: adjust abstraction

* chore: include PR url in commit message

* chore: move generation workflow to after codegen

* chore: spread generation only on main

* chore: spread generation only when codegen is successful

* chore: spread generation as a part of codegen

* chore: apply author name and email to commits

* chore: include coauthor in commit message

* chore: add tests

* chore: add tests
  • Loading branch information
eunjae-lee authored Mar 21, 2022
1 parent ca222a7 commit 6a1b653
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 15 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,16 @@ jobs:
job: codegen

- name: Push generated code
id: pushGeneratedCode
run: yarn workspace scripts pushGeneratedCode
env:
GITHUB_TOKEN: ${{ secrets.TOKEN_GENERATE_BOT }}
PR_NUMBER: ${{ github.event.number }}

- name: Spread generation to each repository
if: |
steps.pushGeneratedCode.exitcode == 0 &&
github.ref == 'refs/heads/main'
run: yarn workspace scripts spreadGeneration
env:
GITHUB_TOKEN: ${{ secrets.TOKEN_RELEASE_BOT }}
38 changes: 38 additions & 0 deletions scripts/__tests__/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import execa from 'execa';

import { gitCommit } from '../common';

jest.mock('execa');

describe('gitCommit', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('commits with message', () => {
gitCommit({ message: 'chore: does something' });
expect(execa).toHaveBeenCalledTimes(1);
expect(execa).toHaveBeenCalledWith(
'git',
['commit', '-m', 'chore: does something'],
{ cwd: expect.any(String) }
);
});

it('commits with co-author', () => {
gitCommit({
message: 'chore: does something',
coauthor: { name: 'some', email: 'random@person.com' },
});
expect(execa).toHaveBeenCalledTimes(1);
expect(execa).toHaveBeenCalledWith(
'git',
[
'commit',
'-m',
'chore: does something\n\n\nCo-authored-by: some <random@person.com>',
],
{ cwd: expect.any(String) }
);
});
});
48 changes: 48 additions & 0 deletions scripts/ci/codegen/__tests__/spreadGeneration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { LANGUAGES } from '../../../common';
import { decideWhereToSpread, cleanUpCommitMessage } from '../spreadGeneration';

describe('spread generation', () => {
it('skips in case of release commit', () => {
expect(decideWhereToSpread('chore: release 2022-03-15')).toEqual([]);
});

it('spreads to all if scope is missing', () => {
expect(decideWhereToSpread('chore: do something')).toEqual(LANGUAGES);
});

it('spreads to javascript if the scope is javascript', () => {
expect(decideWhereToSpread('fix(javascript): fix something')).toEqual([
'javascript',
]);
});

it('spreads to all if scope is not specific language', () => {
['cts', 'spec', 'script', 'ci'].forEach((scope) => {
expect(decideWhereToSpread(`fix(${scope}): fix something`)).toEqual(
LANGUAGES
);
});
});

it('removes pull-request number from commit message', () => {
expect(
cleanUpCommitMessage(`feat(ci): make ci push generated code (#244)`)
).toEqual(
`feat(ci): make ci push generated code\n\nhttps://github.com/algolia/api-clients-automation/pull/244`
);
});

it('keeps the commit message even if it does not have PR number', () => {
const commitMessage = `feat(ci): make ci push generated code`;
expect(cleanUpCommitMessage(commitMessage)).toEqual(commitMessage);
});

it('cleans up correctly even if the title contains a url', () => {
const commitMessage = `fix(java): solve oneOf using a custom generator https://algolia.atlassian.net/browse/APIC-123 (#200)`;
expect(cleanUpCommitMessage(commitMessage)).toMatchInlineSnapshot(`
"fix(java): solve oneOf using a custom generator https://algolia.atlassian.net/browse/APIC-123
https://github.com/algolia/api-clients-automation/pull/200"
`);
});
});
75 changes: 75 additions & 0 deletions scripts/ci/codegen/spreadGeneration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { gitCommit, LANGUAGES, run, toAbsolutePath } from '../../common';
import { getLanguageFolder } from '../../config';
import {
cloneRepository,
configureGitHubAuthor,
OWNER,
REPO,
} from '../../release/common';

const GENERATED_MAIN_BRANCH = `generated/main`;

export function decideWhereToSpread(commitMessage: string): string[] {
if (commitMessage.startsWith('chore: release')) {
return [];
}

const result = commitMessage.match(/(.+)\((.+)\):/);
if (!result) {
// no scope
return LANGUAGES;
}

const scope = result[2];
return LANGUAGES.includes(scope) ? [scope] : LANGUAGES;
}

export function cleanUpCommitMessage(commitMessage: string): string {
const result = commitMessage.match(/(.+)\s\(#(\d+)\)$/);
if (!result) {
return commitMessage;
}

return [
result[1],
`https://github.com/${OWNER}/${REPO}/pull/${result[2]}`,
].join('\n\n');
}

async function spreadGeneration(): Promise<void> {
if (!process.env.GITHUB_TOKEN) {
throw new Error('Environment variable `GITHUB_TOKEN` does not exist.');
}

const lastCommitMessage = await run(`git log -1 --format="%s"`);
const name = (await run(`git log -1 --format="%an"`)).trim();
const email = (await run(`git log -1 --format="%ae"`)).trim();
const commitMessage = cleanUpCommitMessage(lastCommitMessage);
const langs = decideWhereToSpread(lastCommitMessage);

await run(`git checkout ${GENERATED_MAIN_BRANCH}`);

for (const lang of langs) {
const { tempGitDir } = await cloneRepository({
lang,
githubToken: process.env.GITHUB_TOKEN,
tempDir: process.env.RUNNER_TEMP!,
});

const clientPath = toAbsolutePath(getLanguageFolder(lang));
await run(`cp -r ${clientPath}/ ${tempGitDir}`);

await configureGitHubAuthor(tempGitDir);
await run(`git add .`, { cwd: tempGitDir });
await gitCommit({
message: commitMessage,
coauthor: { name, email },
cwd: tempGitDir,
});
await run(`git push`, { cwd: tempGitDir });
}
}

if (require.main === module) {
spreadGeneration();
}
28 changes: 28 additions & 0 deletions scripts/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,34 @@ export async function runIfExists(
return '';
}

export async function gitCommit({
message,
coauthor,
cwd = ROOT_DIR,
}: {
message: string;
coauthor?: {
name: string;
email: string;
};
cwd?: string;
}): Promise<void> {
await execa(
'git',
[
'commit',
'-m',
message +
(coauthor
? `\n\n\nCo-authored-by: ${coauthor.name} <${coauthor.email}>`
: ''),
],
{
cwd,
}
);
}

export async function buildCustomGenerators(verbose: boolean): Promise<void> {
const spinner = createSpinner('building custom generators', verbose).start();
await run('./gradle/gradlew --no-daemon -p generators assemble', {
Expand Down
1 change: 1 addition & 0 deletions scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"processRelease": "ts-node release/process-release.ts",
"pushGeneratedCode": "ts-node ci/codegen/pushGeneratedCode.ts",
"cleanGeneratedBranch": "ts-node ci/codegen/cleanGeneratedBranch.ts",
"spreadGeneration": "ts-node ci/codegen/spreadGeneration.ts",
"upsertGenerationComment": "ts-node ci/codegen/upsertGenerationComment.ts",
"test": "jest"
},
Expand Down
25 changes: 24 additions & 1 deletion scripts/release/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import config from '../../config/release.config.json';
import { run } from '../common';
import { getGitHubUrl, run } from '../common';

export const RELEASED_TAG = config.releasedTag;
export const MAIN_BRANCH = config.mainBranch;
Expand Down Expand Up @@ -36,3 +36,26 @@ export async function configureGitHubAuthor(cwd?: string): Promise<void> {
await run(`git config user.name "${name}"`, { cwd });
await run(`git config user.email "${email}"`, { cwd });
}

export async function cloneRepository({
lang,
githubToken,
tempDir,
}: {
lang: string;
githubToken: string;
tempDir: string;
}): Promise<{ tempGitDir: string }> {
const targetBranch = getTargetBranch(lang);

const gitHubUrl = getGitHubUrl(lang, { token: githubToken });
const tempGitDir = `${tempDir}/${lang}`;
await run(`rm -rf ${tempGitDir}`);
await run(
`git clone --depth 1 --branch ${targetBranch} ${gitHubUrl} ${tempGitDir}`
);

return {
tempGitDir,
};
}
31 changes: 17 additions & 14 deletions scripts/release/process-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
run,
exists,
getGitHubUrl,
gitCommit,
} from '../common';
import { getLanguageFolder } from '../config';

Expand All @@ -19,8 +20,8 @@ import {
OWNER,
REPO,
getMarkdownSection,
getTargetBranch,
configureGitHubAuthor,
cloneRepository,
} from './common';
import TEXT from './text';

Expand Down Expand Up @@ -163,43 +164,45 @@ async function processRelease(): Promise<void> {
await run(`git add ${changelogPath}`);
}

// We push commits from submodules AFTER all the generations are done.
// We push commits to each repository AFTER all the generations are done.
// Otherwise, we will end up having broken release.
for (const lang of langsToReleaseOrUpdate) {
const clientPath = toAbsolutePath(getLanguageFolder(lang));
const targetBranch = getTargetBranch(lang);

const gitHubUrl = getGitHubUrl(lang, { token: process.env.GITHUB_TOKEN });
const tempGitDir = `${process.env.RUNNER_TEMP}/${lang}`;
await run(`rm -rf ${tempGitDir}`);
await run(
`git clone --depth 1 --branch ${targetBranch} ${gitHubUrl} ${tempGitDir}`
);
const { tempGitDir } = await cloneRepository({
lang,
githubToken: process.env.GITHUB_TOKEN,
tempDir: process.env.RUNNER_TEMP!,
});

const clientPath = toAbsolutePath(getLanguageFolder(lang));
await run(`cp -r ${clientPath}/ ${tempGitDir}`);

await configureGitHubAuthor(tempGitDir);
await run(`git add .`, { cwd: tempGitDir });

const { next, dateStamp } = versionsToRelease[lang];

if (willReleaseLibrary(lang)) {
await execa('git', ['commit', '-m', `chore: release ${next}`], {
await gitCommit({
message: `chore: release ${next}`,
cwd: tempGitDir,
});
if (process.env.VERSION_TAG_ON_RELEASE === 'true') {
await execa('git', ['tag', `v${next}`], { cwd: tempGitDir });
await run(`git push --tags`, { cwd: tempGitDir });
}
} else {
await execa('git', ['commit', '-m', `chore: update repo ${dateStamp}`], {
await gitCommit({
message: `chore: update repo ${dateStamp}`,
cwd: tempGitDir,
});
}
await run(`git push`, { cwd: tempGitDir });
}

// Commit and push from the monorepo level.
await execa('git', ['commit', '-m', `chore: release ${getDateStamp()}`]);
await gitCommit({
message: `chore: release ${getDateStamp()}`,
});
await run(`git push`);

// remove old `released` tag
Expand Down

0 comments on commit 6a1b653

Please sign in to comment.