Skip to content

chore(ci): push to each repository on PR merge #257

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

Merged
merged 17 commits into from
Mar 21, 2022
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
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: '[email protected]' },
});
expect(execa).toHaveBeenCalledTimes(1);
expect(execa).toHaveBeenCalledWith(
'git',
[
'commit',
'-m',
'chore: does something\n\n\nCo-authored-by: some <[email protected]>',
],
{ 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