diff --git a/.cnb.yml b/.cnb.yml index e69de29..83a0ac2 100644 --- a/.cnb.yml +++ b/.cnb.yml @@ -0,0 +1,20 @@ +.crontab-job: &crontab-job + - docker: + image: docker.cnb.cool/kevisual/dev-env/ubuntu-bun:latest + runner: + cpus: 2 + imports: + - https://cnb.cool/kevisual/env/-/blob/main/.env.development + - https://cnb.cool/kevisual/env/-/blob/main/ssh.yml + - https://cnb.cool/kevisual/env/-/blob/main/ssh-config.yml + stages: + - name: 检测版本更新 + script: | + bun run src/cli.ts cnb sync + timeout: 20s + +main: + "crontab: 0 11,23 * * *": !reference [.crontab-job] + +$: + push: !reference [.crontab-job] \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..e69de29 diff --git a/src/app.ts b/src/app.ts index 60bbd71..d86813f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,5 @@ import { App } from "@kevisual/router"; -import { useContextKey } from "@kevisual/context"; +import { useContextKey, useKey } from "@kevisual/context"; import { CNB } from "@kevisual/cnb"; import { Gitea } from '@kevisual/gitea'; import path from "node:path"; @@ -11,13 +11,13 @@ export const app = useContextKey("app", () => { }); export const cnb = useContextKey("cnb", () => { - const token = useContextKey("CNB_API_KEY") || useContextKey('CNB_TOKEN') + const token = useKey("CNB_API_KEY") || useKey('CNB_TOKEN') return new CNB({ token }); }); export const gitea = useContextKey('gitea', () => { - const GITEA_TOKEN = useContextKey("GITEA_TOKEN") - const GITEA_URL = useContextKey("GITEA_URL") + const GITEA_TOKEN = useKey("GITEA_TOKEN") + const GITEA_URL = useKey("GITEA_URL") return new Gitea({ token: GITEA_TOKEN, baseURL: GITEA_URL, diff --git a/src/index.ts b/src/index.ts index 86d9330..9613780 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './app.ts'; -import './routes/cnb.ts'; \ No newline at end of file +import './routes/cnb.ts'; +import './routes/sync.ts'; \ No newline at end of file diff --git a/src/routes/cnb.ts b/src/routes/cnb.ts index 823f028..c85b1a8 100644 --- a/src/routes/cnb.ts +++ b/src/routes/cnb.ts @@ -1,18 +1,37 @@ import { app, cnb } from '../app'; import dayjs from 'dayjs'; +import { z } from 'zod'; + +const repoSchema = z.object({ + path: z.string().describe('仓库路径'), + name: z.string().describe('仓库名称'), + web_url: z.string().describe('仓库链接'), + description: z.string().describe('仓库描述'), + topics: z.array(z.string()).describe('仓库标签'), + giteaUrl: z.string().optional().describe('gitea仓库链接') +}).describe('仓库信息'); +export type Repo = z.infer; + app.route({ path: 'cnb', key: 'list-today', description: '获取今日更新的仓库列表', }).define(async (ctx) => { - const res = await cnb.repo.getRepoList({ page_size: 100 }); - const today = dayjs().format('YYYY-MM-DD'); + const res = await cnb.repo.getRepoList({ status: 'active', page_size: 100 }); const list = res.data || []; + const twelveHoursAgo = dayjs().subtract(12, 'hour'); const todayList = list.filter(item => { - const updatedAt = dayjs(item.updated_at).format('YYYY-MM-DD'); - return updatedAt === today; + const updatedAt = dayjs(item.updated_at); + return updatedAt.isAfter(twelveHoursAgo); }); + const repositories = todayList.map(item => ({ + path: item.path, + name: item.name, + web_url: item.web_url, + description: item.description, + topics: item.topics ? item.topics.split(',').filter(Boolean) : [], + })).filter(item => item.topics.includes('gitea')); ctx.body = { - list: todayList, + list: repositories } }).addTo(app) \ No newline at end of file diff --git a/src/routes/sync.ts b/src/routes/sync.ts new file mode 100644 index 0000000..608ec22 --- /dev/null +++ b/src/routes/sync.ts @@ -0,0 +1,68 @@ +import { app, gitea, rootPath } from '../app' +import { Repo } from './cnb'; +import { execSync } from 'node:child_process'; +import path from 'node:path'; +import fs from 'fs'; +import { useKey } from '@kevisual/context'; + +app.route({ + path: 'repo', + key: 'sync', + description: '同步仓库数据', +}).define(async (ctx) => { + const res = await app.run({ path: 'cnb', key: 'list-today' }); + if (res.code === 200) { + const list: Repo[] = res.data.list || []; + let syncList: Repo[] = []; + for (const item of list) { + try { + const [owner, repo] = item.path.split('/'); + const giteaRepo = await gitea.repo.getRepo(owner, repo); + const giteaUsername = 'oauth2'; + const giteaPassword = useKey('GITEA_TOKEN'); + item.giteaUrl = `https://${giteaUsername}:${giteaPassword}@git.xiongxiao.me/${owner}/${repo}.git`; + if (giteaRepo.code === 200) { + // 已经存在了 + } else { + await gitea.repo.createRepo({ + name: owner + '/' + repo, + description: item.description, + }); + } + syncList.push(item); + } catch (err) { + console.error(`处理 ${item.path} 失败`, item); + } + // 开始同步 + for (const repo of syncList) { + try { + await sync(repo); + } catch (err) { + console.error(`同步 ${repo.path} 失败`, repo); + } + } + } + } +}).addTo(app) + +const sync = async (repo: Repo) => { + const cwd = path.join(rootPath, repo.name); + if (!fs.existsSync(cwd)) { + execSync(`git clone ${repo.web_url} ${cwd}`); + } + // 添加一个remote 叫做gitea的远程仓库,指向gitea的地址 + const remoteUrl = repo.giteaUrl!; + try { + execSync(`git remote add gitea ${remoteUrl}`, { cwd }); + } catch (err) { + // 已经添加过了 + } + try { + // 拉取最新的代码 + execSync(`git pull origin main`, { cwd }); + // 推送到gitea + execSync(`git push gitea main`, { cwd }); + } catch (err) { + console.error(`同步推送 ${repo.path} 失败`, repo); + } +} \ No newline at end of file