ci: release building

This commit is contained in:
alina 🌸 2023-10-31 21:17:39 +03:00
parent 103901ed21
commit 958f7ff81c
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
29 changed files with 321 additions and 29 deletions

View file

@ -17,6 +17,7 @@ concurrency:
jobs: jobs:
build: build:
if: github.repository == 'mtcute/mtcute' # do not run on forks
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository

71
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,71 @@
name: Run release
on:
workflow_dispatch:
inputs:
packages:
description: 'Packages to release (comma separated names, `all` or `updated`)'
required: true
default: 'updated'
kind:
description: 'Release kind (major, minor, patch)'
required: true
default: 'patch'
type: choice
options:
- major
- minor
- patch
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
token: ${{ secrets.BOT_PAT }}
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: "18"
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- run: pnpm install --frozen-lockfile
- name: 'TL codegen'
run: pnpm -C packages/tl run gen-code
- name: Initialize configs
run: |
git config user.name "mtcute-bot"
git config user.email mtcute-bot@tei.su
npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
- name: Find packages to publish
id: find
run: node scripts/find-updated-packages.js ${{ inputs.kind }} ${{ inputs.packages }}
- name: Bump versions
id: bump
run: node scripts/bump-version.js ${{ inputs.kind }} ${{ steps.find.outputs.modified }}
- name: Commit version bumps
run: |
git commit -am "v${{ steps.bump.outputs.version }}"
git push
- name: Build pacakges and publish to NPM
id: build
env:
GH_RELEASE: 1
run: node scripts/publish.js ${{ steps.find.outputs.modified }}
- name: GitHub Release
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: v${{ steps.bump.outputs.version }}
name: v${{ steps.bump.outputs.version }}
artifacts: ${{ steps.build.outputs.tarballs }}}
draft: false
prerelease: false

View file

@ -10,6 +10,7 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.actor != 'mtcute-bot' # do not after release
strategy: strategy:
matrix: matrix:

1
.npmrc
View file

@ -1 +0,0 @@
@mtcute:registry=https://npm.tei.su

View file

@ -30,6 +30,7 @@ case "$method" in
pnpm install pnpm install
;; ;;
"ci") "ci")
set -eaux
chmod -R 777 .verdaccio chmod -R 777 .verdaccio
docker compose up -d verdaccio docker compose up -d verdaccio
docker compose run --rm --build build docker compose run --rm --build build

View file

@ -1,7 +1,7 @@
{ {
"name": "mtcute", "name": "mtcute",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "Type-safe library for MTProto (Telegram API) for browser and NodeJS", "description": "Type-safe library for MTProto (Telegram API) for browser and NodeJS",
"license": "MIT", "license": "MIT",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/client", "name": "@mtcute/client",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "High-level API over @mtcute/core", "description": "High-level API over @mtcute/core",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/core", "name": "@mtcute/core",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "Core functions and base MTProto client", "description": "Core functions and base MTProto client",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/create-bot", "name": "@mtcute/create-bot",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "Bot starter kit for mtcute", "description": "Bot starter kit for mtcute",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1 +0,0 @@
@mtcute:registry=https://npm.tei.su

View file

@ -1,7 +1,7 @@
{ {
"name": "{{ name }}", "name": "{{ name }}",
"license": "MIT", "license": "MIT",
"version": "0.1.0", "version": "0.0.0",
{{#if features.typescript}} {{#if features.typescript}}
"main": "dist/index.js", "main": "dist/index.js",
{{else}} {{else}}

View file

@ -1,6 +1,6 @@
{ {
"name": "@mtcute/crypto-node", "name": "@mtcute/crypto-node",
"version": "0.1.0", "version": "0.0.0",
"description": "Native crypto implementation for NodeJS", "description": "Native crypto implementation for NodeJS",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"main": "src/index.ts", "main": "src/index.ts",

View file

@ -1,5 +0,0 @@
{
"name": "@mtcute/crypto",
"private": true,
"version": "1.0.0"
}

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/dispatcher", "name": "@mtcute/dispatcher",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "Updates dispatcher and bot framework for @mtcute/client", "description": "Updates dispatcher and bot framework for @mtcute/client",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/file-id", "name": "@mtcute/file-id",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "Support for TDLib and Bot API file ID for mtcute", "description": "Support for TDLib and Bot API file ID for mtcute",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/html-parser", "name": "@mtcute/html-parser",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "HTML entities parser for mtcute", "description": "HTML entities parser for mtcute",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/http-proxy", "name": "@mtcute/http-proxy",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "HTTP(S) proxy support for mtcute", "description": "HTTP(S) proxy support for mtcute",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/i18n", "name": "@mtcute/i18n",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "I18n for mtcute", "description": "I18n for mtcute",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/markdown-parser", "name": "@mtcute/markdown-parser",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "Markdown entities parser for mtcute", "description": "Markdown entities parser for mtcute",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/mtproxy", "name": "@mtcute/mtproxy",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "MTProto proxy (MTProxy) support for mtcute", "description": "MTProto proxy (MTProxy) support for mtcute",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/node", "name": "@mtcute/node",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "Meta-package for Node JS", "description": "Meta-package for Node JS",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/socks-proxy", "name": "@mtcute/socks-proxy",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "SOCKS4/5 proxy support for mtcute", "description": "SOCKS4/5 proxy support for mtcute",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/sqlite", "name": "@mtcute/sqlite",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "SQLite-based storage for mtcute", "description": "SQLite-based storage for mtcute",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/tl-runtime", "name": "@mtcute/tl-runtime",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "Runtime for TL", "description": "Runtime for TL",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/tl-utils", "name": "@mtcute/tl-utils",
"private": true, "private": true,
"version": "0.1.0", "version": "0.0.0",
"description": "Utils for working with TL schema", "description": "Utils for working with TL schema",
"author": "Alina Sireneva <alina@tei.su>", "author": "Alina Sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",

View file

@ -151,7 +151,17 @@ function buildPackageJson() {
const value = dependencies[name] const value = dependencies[name]
if (value.startsWith('workspace:')) { if (value.startsWith('workspace:')) {
dependencies[name] = value.replace('workspace:', '') if (value !== 'workspace:^') {
throw new Error(
`Cannot replace workspace dependency ${name} with ${value} - only workspace:^ is supported`,
)
}
if (!name.startsWith('@mtcute/')) {
throw new Error(`Cannot replace workspace dependency ${name} - only @mtcute/* is supported`)
}
const depVersion = require(path.join(packageDir, '..', name.slice(8), 'package.json')).version
dependencies[name] = `^${depVersion}`
} }
} }
} }

81
scripts/bump-version.js Normal file
View file

@ -0,0 +1,81 @@
const fs = require('fs')
const path = require('path')
const semver = require('semver')
function collectPackageJsons() {
return fs
.readdirSync(path.join(__dirname, '../packages'))
.filter((s) => !s.startsWith('.'))
.map((name) => {
try {
return JSON.parse(fs.readFileSync(path.join(__dirname, '../packages', name, 'package.json'), 'utf-8'))
} catch (e) {
if (e.code !== 'ENOENT') throw e
return null
}
})
.filter(Boolean)
}
function bumpVersions(packages, kind) {
const pkgJsons = collectPackageJsons()
const maxVersion = pkgJsons
.filter((it) => it.name !== '@mtcute/tl')
.map((it) => it.version)
.sort(semver.rcompare)[0]
const nextVersion = semver.inc(maxVersion, kind)
console.log('[i] Bumping versions to %s', nextVersion)
for (const pkg of packages) {
const pkgJson = pkgJsons.find((it) => it.name === `@mtcute/${pkg}`)
if (!pkgJson) {
console.error(`Package ${pkg} not found!`)
process.exit(1)
}
pkgJson.version = nextVersion
fs.writeFileSync(
path.join(__dirname, '../packages', pkg, 'package.json'),
JSON.stringify(pkgJson, null, 4) + '\n',
)
}
const rootPkgJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'))
rootPkgJson.version = nextVersion
fs.writeFileSync(path.join(__dirname, '../package.json'), JSON.stringify(rootPkgJson, null, 4) + '\n')
return nextVersion
}
if (require.main === module) {
const kind = process.argv[2]
const packages = process.argv[3]
if (!packages || !kind) {
console.log('Usage: bump-version.js <major|minor|patch> <package1,package2>')
process.exit(1)
}
const packagesList = packages.split(',')
if (packagesList.length === 0) {
console.error('No packages specified!')
process.exit(1)
}
if (kind === 'major' && packagesList.length !== collectPackageJsons().length) {
console.error('Cannot bump major version only for some packages!')
process.exit(1)
}
const ver = bumpVersions(packagesList, kind)
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${ver}${require('os').EOL}`)
}
}
module.exports = { bumpVersions }

View file

@ -0,0 +1,115 @@
const cp = require('child_process')
const fs = require('fs')
const path = require('path')
const { listPackages } = require('./publish')
function getLatestTag() {
try {
const res = cp.execSync('git describe --abbrev=0 --tags', { encoding: 'utf8', stdio: 'pipe' }).trim()
return res
} catch (e) {
if (e.stderr.match(/^fatal: (No names found|No tags can describe)/i)) {
// no tags found, let's just return the first commit
return cp.execSync('git rev-list --max-parents=0 HEAD', { encoding: 'utf8' }).trim()
}
throw e
}
}
function findChangedFilesSince(tag, until = 'HEAD') {
return cp.execSync(`git diff --name-only ${tag} ${until}`, { encoding: 'utf8', stdio: 'pipe' }).trim().split('\n')
}
getTsconfigFiles.cache = {}
function getTsconfigFiles(pkg) {
if (!fs.existsSync(path.join(__dirname, `../packages/${pkg}/tsconfig.json`))) {
throw new Error(`[!] ${pkg} does not have a tsconfig.json`)
}
if (pkg in getTsconfigFiles.cache) return getTsconfigFiles.cache[pkg]
console.log('[i] Getting tsconfig files for %s', pkg)
const res = cp.execSync('pnpm exec tsc --showConfig', {
encoding: 'utf8',
stdio: 'pipe',
cwd: path.join(__dirname, `../packages/${pkg}`),
})
const json = JSON.parse(res)
return (getTsconfigFiles.cache[pkg] = json.files.map((it) => it.replace(/^\.\//, '')))
}
function isMeaningfulChange(pkg, path) {
if (getTsconfigFiles(pkg).indexOf(path) > -1) return true
// some magic heuristics stuff
if (path.match(/\.md$/i)) return false
if (path.match(/^\/(scripts|dist|tests|private)/i)) return false
// to be safe
return true
}
function findChangedPackagesSince(tag, until) {
const packages = new Set(listPackages())
const changedFiles = findChangedFilesSince(tag, until)
const changedPackages = new Set()
for (const file of changedFiles) {
const [dir, pkgname, ...rest] = file.split('/')
if (dir !== 'packages') continue
if (!packages.has(pkgname)) continue
// already checked, no need to check again
if (changedPackages.has(pkgname)) continue
const relpath = rest.join('/')
if (isMeaningfulChange(pkgname, relpath)) {
changedPackages.add(pkgname)
}
}
return Array.from(changedPackages)
}
module.exports = { findChangedPackagesSince, getLatestTag }
if (require.main === module && process.env.CI && process.env.GITHUB_OUTPUT) {
const kind = process.argv[2]
const input = process.argv[3]
if (!input) {
// for simpler flow, one can pass all or package list as the first argument,
// and they will be returned as is, so that we can later simply
// use the outputs of this script
console.log('Usage: find-updated-packages.js <packages>')
process.exit(1)
}
if (kind === 'major' && input !== 'all') {
throw new Error('For major releases, all packages must be published')
}
console.log('[i] Determining packages to publish...')
let res
if (input === 'all') {
res = listPackages()
} else if (input === 'updated') {
const tag = getLatestTag()
console.log('[i] Latest tag is %s', tag)
res = findChangedPackagesSince(tag)
} else {
res = input.split(',')
}
console.log('[i] Will publish:', res)
fs.appendFileSync(process.env.GITHUB_OUTPUT, `modified=${res.join(',')}${require('os').EOL}`)
}

View file

@ -2,8 +2,7 @@ const fs = require('fs')
const path = require('path') const path = require('path')
const cp = require('child_process') const cp = require('child_process')
// const REGISTRY = 'https://registry.npmjs.org' const REGISTRY = process.env.REGISTRY || 'https://registry.npmjs.org'
const REGISTRY = process.env.REGISTRY || 'https://npm.tei.su/'
exports.REGISTRY = REGISTRY exports.REGISTRY = REGISTRY
async function checkVersion(name, version, retry = 0) { async function checkVersion(name, version, retry = 0) {
@ -82,6 +81,8 @@ async function main(arg = process.argv[2]) {
console.log('[i] Using registry %s', REGISTRY) console.log('[i] Using registry %s', REGISTRY)
const published = []
if (arg === 'all' || arg === 'updated') { if (arg === 'all' || arg === 'updated') {
for (const pkg of listPackages()) { for (const pkg of listPackages()) {
if (arg === 'updated') { if (arg === 'updated') {
@ -95,9 +96,27 @@ async function main(arg = process.argv[2]) {
} }
await publishSinglePackage(pkg) await publishSinglePackage(pkg)
published.push(pkg)
} }
} else { } else {
await publishSinglePackage(arg) for (const pkg of arg.split(',')) {
await publishSinglePackage(pkg)
published.push(pkg)
}
}
if (process.env.GH_RELEASE) {
// we should also generate tgz files for all published packages
// for a github release, and also generate a title
const tarballs = []
for (const pkg of published) {
const dir = path.join(__dirname, '../packages', pkg, 'dist')
const tar = cp.execSync('npm pack -q', { cwd: dir })
tarballs.push(path.join(dir, tar.toString().trim()))
}
fs.writeFileSync(process.env.GITHUB_OUTPUT, `tarballs=${tarballs.join(',')}\n`, { flag: 'a' })
} }
} }