-
Notifications
You must be signed in to change notification settings - Fork 17
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
chore: automate release #120
Merged
Merged
Changes from 40 commits
Commits
Show all changes
50 commits
Select commit
Hold shift + click to select a range
5c44888
wip
eunjae-lee 1675bf8
use dotenv
eunjae-lee af8170e
push after pull
eunjae-lee e263c91
checking working directory
eunjae-lee 59065d1
fix github info
eunjae-lee afe00ca
update pr description
eunjae-lee a46ddc6
setup github action
eunjae-lee a603301
update script in package.json
eunjae-lee fc0f186
change pr desc
eunjae-lee eea302e
get pull-request number from action
eunjae-lee 8dbb6d4
use issue instead of pr
eunjae-lee e0c7bb7
check released tag
eunjae-lee 741cf26
check released tag
eunjae-lee 49c74c3
update gh action condition
eunjae-lee 8652040
fix
eunjae-lee c74b2de
.
eunjae-lee 60c3cc4
another fix
eunjae-lee 3712cfc
.
eunjae-lee fd76229
fail when not approved
eunjae-lee b60b1ef
commit versions
eunjae-lee 3b119d1
change config
eunjae-lee 28d1198
update git config
eunjae-lee 0b9931e
update git config
eunjae-lee 1160d7b
update command
eunjae-lee 703d47f
clean up
eunjae-lee cdd385c
chore: update versions
2d33b1a
prepend changelog
8bdb9ed
commit changelog
f09b622
Revert "chore: update versions"
acb8492
clean up
3aa36a8
Merge branch 'main' into main
eunjae-lee e167e52
Merge branch 'main' into main
eunjae-lee 8b77ef1
Merge branch 'main' into main
eunjae-lee 968719b
Merge branch 'main' into main
eunjae-lee f1f8f7c
Update scripts/release/process-release.js
eunjae-lee 6a34b36
chore: clean up common
eunjae-lee 3d25889
chore: throw instead of exit
eunjae-lee ed5a9f5
chore: return early when env var does not exist
eunjae-lee 7971ac9
chore: clean up texts
eunjae-lee 4b9acfd
Merge branch 'main' into main
eunjae-lee 5ce225f
chore: convert to TS
eunjae-lee d097db7
Merge branch 'main' of github.com:eunjae-lee/api-clients-automation
eunjae-lee f6c367b
chore: import instead of read json
eunjae-lee ba9017d
chore: remove unused dotenv from the root level
eunjae-lee 98f6706
Merge branch 'main' into main
eunjae-lee 1d83f15
chore: rename config.json to release.config.json
eunjae-lee 47a2307
chore: pin devDependencies
eunjae-lee b9e765d
chore: remove shebang
eunjae-lee 5c1fdc6
chore: use trim instead of arbitrary number
eunjae-lee 5bb53d9
Merge branch 'main' into main
eunjae-lee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
name: Process release | ||
on: | ||
issues: | ||
types: | ||
- closed | ||
jobs: | ||
build: | ||
name: Release | ||
runs-on: ubuntu-20.04 | ||
if: "startsWith(github.event.issue.title, 'chore: release')" | ||
steps: | ||
- uses: actions/checkout@v2 | ||
with: | ||
fetch-depth: 0 | ||
|
||
- name: Setup | ||
id: setup | ||
uses: ./.github/actions/setup | ||
|
||
- run: ./scripts/release/process-release.js | ||
env: | ||
EVENT_NUMBER: ${{ github.event.issue.number }} | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"release": { | ||
"releasedTag": "released", | ||
"mainBranch": "main", | ||
"owner": "algolia", | ||
"repo": "api-clients-automation" | ||
} | ||
} |
Empty file.
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/* eslint-disable @typescript-eslint/no-var-requires */ | ||
/* eslint-disable import/no-commonjs */ | ||
eunjae-lee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const execa = require('execa'); // https://github.com/sindresorhus/execa/tree/v5.1.1 | ||
const config = require('../../config.json'); | ||
const openapitools = require('../../openapitools.json'); | ||
|
||
function run(command, { errorMessage = undefined } = {}) { | ||
let result; | ||
try { | ||
result = execa.commandSync(command); | ||
} catch (err) { | ||
if (errorMessage) { | ||
throw new Error(`[ERROR] ${errorMessage}`); | ||
} else { | ||
throw err; | ||
} | ||
} | ||
return result.stdout; | ||
} | ||
|
||
const LANGS = [ | ||
eunjae-lee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
...new Set( | ||
Object.keys(openapitools['generator-cli'].generators).map( | ||
(key) => key.split('-')[0] | ||
) | ||
), | ||
]; | ||
|
||
module.exports = { | ||
RELEASED_TAG: config.release.releasedTag, | ||
MAIN_BRANCH: config.release.mainBranch, | ||
OWNER: config.release.owner, | ||
REPO: config.release.repo, | ||
LANGS, | ||
run, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
#!/usr/bin/env node | ||
/* eslint-disable no-console */ | ||
/* eslint-disable import/no-commonjs */ | ||
/* eslint-disable @typescript-eslint/no-var-requires */ | ||
const fs = require('fs'); | ||
|
||
const { Octokit } = require('@octokit/rest'); | ||
const dotenv = require('dotenv'); | ||
const semver = require('semver'); | ||
|
||
const { | ||
RELEASED_TAG, | ||
MAIN_BRANCH, | ||
OWNER, | ||
REPO, | ||
LANGS, | ||
run, | ||
} = require('./common'); | ||
|
||
dotenv.config(); | ||
|
||
const TEXT = require('./text'); | ||
|
||
function readVersions() { | ||
const versions = {}; | ||
|
||
const generators = JSON.parse( | ||
fs.readFileSync('openapitools.json').toString() | ||
eunjae-lee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
)['generator-cli'].generators; | ||
|
||
Object.keys(generators).forEach((generator) => { | ||
const lang = generator.split('-')[0]; | ||
if (!versions[lang]) { | ||
versions[lang] = { | ||
current: generators[generator].additionalProperties.packageVersion, | ||
langName: lang, | ||
next: undefined, | ||
}; | ||
} | ||
}); | ||
return versions; | ||
} | ||
|
||
if (!process.env.GITHUB_TOKEN) { | ||
throw new Error('Environment variable `GITHUB_TOKEN` does not exist.'); | ||
} | ||
|
||
if (run('git rev-parse --abbrev-ref HEAD') !== MAIN_BRANCH) { | ||
throw new Error( | ||
`You can run this script only from \`${MAIN_BRANCH}\` branch.` | ||
); | ||
} | ||
|
||
if (run('git status --porcelain')) { | ||
throw new Error( | ||
'Working directory is not clean. Commit all the changes first.' | ||
); | ||
} | ||
|
||
run(`git rev-parse --verify refs/tags/${RELEASED_TAG}`, { | ||
errorMessage: '`released` tag is missing in this repository.', | ||
}); | ||
|
||
// Reading versions from `openapitools.json` | ||
const versions = readVersions(); | ||
|
||
console.log('Pulling from origin...'); | ||
run(`git pull origin ${MAIN_BRANCH}`); | ||
|
||
console.log('Pushing to origin...'); | ||
run(`git push origin ${MAIN_BRANCH}`); | ||
|
||
const commitsWithoutScope = []; | ||
const commitsWithNonLanguageScope = []; | ||
// Reading commits since last release | ||
const latestCommits = run(`git log --oneline ${RELEASED_TAG}..${MAIN_BRANCH}`) | ||
.split('\n') | ||
.filter(Boolean) | ||
.map((commit) => { | ||
const hash = commit.slice(0, 7); | ||
let message = commit.slice(8); | ||
let type = message.slice(0, message.indexOf(':')); | ||
const matchResult = type.match(/(.+)\((.+)\)/); | ||
if (!matchResult) { | ||
commitsWithoutScope.push(commit); | ||
return undefined; | ||
} | ||
message = message.slice(message.indexOf(':') + 2); | ||
type = matchResult[1]; | ||
const lang = matchResult[2]; | ||
|
||
if (!LANGS.includes(lang)) { | ||
commitsWithNonLanguageScope.push(commit); | ||
return undefined; | ||
} | ||
|
||
return { | ||
hash, | ||
type, // `fix` | `feat` | `chore` | ... | ||
lang, // `javascript` | `php` | `java` | ... | ||
message, | ||
raw: commit, | ||
}; | ||
}) | ||
.filter(Boolean); | ||
|
||
console.log('[INFO] Skipping these commits due to lack of language scope:'); | ||
console.log(commitsWithoutScope.map((commit) => ` ${commit}`).join('\n')); | ||
|
||
console.log(''); | ||
console.log('[INFO] Skipping these commits due to wrong scopes:'); | ||
console.log( | ||
commitsWithNonLanguageScope.map((commit) => ` ${commit}`).join('\n') | ||
); | ||
|
||
LANGS.forEach((lang) => { | ||
const commits = latestCommits.filter( | ||
(lastestCommit) => lastestCommit.lang === lang | ||
); | ||
const currentVersion = versions[lang].current; | ||
|
||
if (commits.length === 0) { | ||
versions[lang].next = currentVersion; | ||
versions[lang].noCommit = true; | ||
return; | ||
} | ||
|
||
if (semver.prerelease(currentVersion)) { | ||
// if version is like 0.1.2-beta.1, it increases to 0.1.2-beta.2, even if there's a breaking change. | ||
versions[lang].next = semver.inc(currentVersion, 'prerelease'); | ||
return; | ||
} | ||
|
||
if (commits.some((commit) => commit.message.includes('BREAKING CHANGE'))) { | ||
versions[lang].next = semver.inc(currentVersion, 'major'); | ||
return; | ||
} | ||
|
||
const commitTypes = new Set(commits.map(({ type }) => type)); | ||
if (commitTypes.has('feat')) { | ||
versions[lang].next = semver.inc(currentVersion, 'minor'); | ||
return; | ||
} | ||
|
||
versions[lang].next = semver.inc(currentVersion, 'patch'); | ||
if (!commitTypes.has('fix')) { | ||
versions[lang].skipRelease = true; | ||
} | ||
}); | ||
|
||
const versionChanges = LANGS.map((lang) => { | ||
const { current, next, noCommit, skipRelease, langName } = versions[lang]; | ||
|
||
if (noCommit) { | ||
return `- ~${langName}: v${current} (${TEXT.noCommit})~`; | ||
} | ||
|
||
if (!current) { | ||
return `- ~${langName}: (${TEXT.currentVersionNotFound})~`; | ||
} | ||
|
||
const checked = skipRelease ? ' ' : 'x'; | ||
return [ | ||
`- [${checked}] ${langName}: v${current} -> v${next}`, | ||
skipRelease && TEXT.descriptionForSkippedLang(langName), | ||
] | ||
.filter(Boolean) | ||
.join('\n'); | ||
}).join('\n'); | ||
|
||
const changelogs = LANGS.filter( | ||
(lang) => !versions[lang].noCommit && versions[lang].current | ||
) | ||
.flatMap((lang) => { | ||
if (versions[lang].noCommit) { | ||
return []; | ||
} | ||
|
||
return [ | ||
`### ${versions[lang].langName}`, | ||
...latestCommits | ||
.filter((commit) => commit.lang === lang) | ||
.map((commit) => `- ${commit.raw}`), | ||
]; | ||
}) | ||
.join('\n'); | ||
|
||
const body = [ | ||
TEXT.header, | ||
TEXT.versionChangeHeader, | ||
versionChanges, | ||
TEXT.changelogHeader, | ||
TEXT.changelogDescription, | ||
changelogs, | ||
TEXT.approvalHeader, | ||
TEXT.approval, | ||
].join('\n\n'); | ||
|
||
const octokit = new Octokit({ | ||
auth: `token ${process.env.GITHUB_TOKEN}`, | ||
}); | ||
|
||
octokit.rest.issues | ||
.create({ | ||
owner: OWNER, | ||
repo: REPO, | ||
title: `chore: release ${new Date().toISOString().split('T')[0]}`, | ||
body, | ||
}) | ||
.then((result) => { | ||
const { | ||
data: { number, html_url: url }, | ||
} = result; | ||
|
||
console.log(''); | ||
console.log(`Release issue #${number} is ready for review.`); | ||
console.log(` > ${url}`); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we put it at the root of the repository we could maybe have a more descriptive name, wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we release this actual repo or just the one generated ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't release it, it's just publicly available but we already have a few configs everywhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually wrapped it with the key "release" for the reason. What do you think? Should we have a separate "release.config.json"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you think of user use cases of this config, it's fine to keep it like that, otherwise yep it would make sense to have a
release.config.json
file only