【码动四季】GitHub 到 AtomGit 同步发布实战, 自动同步代码, Tag, Release 附件等
本文为 AtomGit 码动四季·开源同行 征稿活动参与文章
本文以 ClickClaw 为例,讲清楚一个真实问题。
ClickClaw 是一个面向小白用户的 OpenClaw 桌面管理工具,支持 Windows/macOS,提供可视化配置、模型管理、聊天渠道配置和 Skill 技能市场。
项目 AtomGit 仓库地址:https://atomgit.com/clickclaw/clickclaw
在这个项目里,我们使用 GitHub Actions 自动构建 Windows/macOS 安装包;同时因为国内用户从 GitHub 下载较慢,我们将 Tag、Release 与附件自动同步到 AtomGit,并通过官方代理 + hosts 方案优化上传速度。
适用对象:
- Electron 或其他需要产出二进制安装包的项目
- 同时维护 GitHub 与 AtomGit 两个平台仓库的团队
- 希望把“发版 + 跨平台同步”全部自动化的团队
1. 场景与问题
我们的实际发布链路是:
- 项目类型:Electron 桌面应用
- 构建目标:Windows + macOS 安装包
- CI 平台:GitHub Actions 自动构建与发版
- 仓库平台:GitHub + AtomGit 双仓库
上线后遇到的核心问题:
- GitHub 在国内下载 Release 附件速度慢
- 用户在 AtomGit 访问项目时,常拿不到与 GitHub 一致的 Tag/Release/附件
- 手工同步容易漏步骤,尤其是附件
所以我们要做的不只是“代码镜像”,而是完整发布同步:
- 分支同步到 AtomGit
- Tag 同步到 AtomGit
- Release 元数据同步(标题、正文、预发布状态)
- Release 附件同步(安装包等二进制文件)
2. 解决方案设计
2.1 触发方式
工作流支持两种触发:
- 自动触发:当上游发布工作流成功后触发(
workflow_run) - 手动触发:输入指定
tag执行补同步(workflow_dispatch)
2.2 执行流程(自动化)
- 解析待同步 Tag
- 检查 GitHub Release 是否存在
- 拉取全部分支和全部 Tag
- 推送分支和 Tag 到 AtomGit
- 在 AtomGit 创建或更新同名 Release
- 下载 GitHub Release 附件并上传到 AtomGit(已存在同名附件则跳过)
2.3 关键设计点
- 幂等:重复执行不会重复上传同名附件
- 可补偿:自动失败时可手动指定 Tag 重放
- 前置校验:Release 不存在时直接退出,避免误报
- 可观测:日志输出下载/上传进度和关键状态
2.4 上传慢问题的处理(重点)
GitHub Runner 直连 AtomGit 上传附件时可能很慢。
AtomGit 官方提供了代理方案,我们在 workflow 里通过修改 /etc/hosts 加速:
159.138.147.37 api.atomgit.com159.138.147.37 file.atomgit.com159.138.147.37 file.gitcode.com
这一步是 runner 临时生效,任务结束后自动失效。

图:在 workflow 中添加 /etc/hosts 代理解析步骤,用于提升上传稳定性。
3. 前置准备
3.1 AtomGit 仓库准备
先在 AtomGit 创建目标仓库,例如:clickclaw/clickclaw。
3.2 AtomGit Token
创建一个具备仓库写入、Release 读写能力的访问令牌,并根据你的安全策略设置到期时间。
令牌创建地址:https://atomgit.com/setting/token-classic

3.3 GitHub 仓库 Secrets / Variables
路径:Settings -> Secrets and variables -> Actions

图:进入仓库设置后,配置 Secrets 。
必填 Secrets
ATOMGIT_TOKEN:AtomGit TokenATOMGIT_GIT_USERNAME:AtomGit 用户名(Git HTTPS 推送用)ATOMGIT_REPO_OWNER:AtomGit 仓库 owner(例如clickclaw)ATOMGIT_REPO_NAME:AtomGit 仓库名(例如clickclaw)
建议 Variables
ATOMGIT_GIT_HOST:默认atomgit.comATOMGIT_API_BASE:默认https://api.atomgit.com
4. 文件落地路径
在你的仓库添加两个文件:
.github/workflows/sync-atomgit-release.ymlscripts/sync-atomgit-release.js
下面提供完整可复制版本。
5. Workflow 完整代码
文件:.github/workflows/sync-atomgit-release.yml
name: 同步 AtomGit 发布
on:
workflow_run:
workflows: ['构建并发布']
types: [completed]
workflow_dispatch:
inputs:
tag:
description: '要同步的 release tag,例如 v0.2.1'
required: true
type: string
jobs:
sync:
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: 检出触发提交
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || github.event.workflow_run.head_sha }}
fetch-depth: 0
persist-credentials: false
- name: 安装 Node.js 22
uses: actions/setup-node@v6
with:
node-version: 22
- name: 读取版本和标签
id: meta
shell: bash
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
VERSION="${TAG#v}"
else
VERSION="$(node -p "require('./package.json').version")"
TAG="v${VERSION}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- name: 确认 GitHub Release 已存在
id: release_check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
RELEASE_TAG: ${{ steps.meta.outputs.tag }}
run: |
set -euo pipefail
if ! gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "GitHub Release ${RELEASE_TAG} 不存在,跳过 AtomGit 同步。"
echo "exists=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "exists=true" >> "$GITHUB_OUTPUT"
- name: 拉取完整分支和标签
if: steps.release_check.outputs.exists == 'true'
shell: bash
run: |
set -euo pipefail
git fetch origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*'
- name: 配置 AtomGit /etc/hosts 代理解析
if: steps.release_check.outputs.exists == 'true'
shell: bash
run: |
set -euo pipefail
echo "159.138.147.37 api.atomgit.com" | sudo tee -a /etc/hosts
echo "159.138.147.37 file.atomgit.com" | sudo tee -a /etc/hosts
echo "159.138.147.37 file.gitcode.com" | sudo tee -a /etc/hosts
getent hosts api.atomgit.com
getent hosts file.atomgit.com
getent hosts file.gitcode.com
- name: 推送代码和标签到 AtomGit
if: steps.release_check.outputs.exists == 'true'
env:
ATOMGIT_GIT_HOST: ${{ vars.ATOMGIT_GIT_HOST || 'atomgit.com' }}
ATOMGIT_GIT_OWNER: ${{ vars.ATOMGIT_REPO_OWNER || 'clickclaw' }}
ATOMGIT_GIT_REPO: ${{ vars.ATOMGIT_REPO_NAME || 'clickclaw' }}
ATOMGIT_GIT_USERNAME: ${{ secrets.ATOMGIT_GIT_USERNAME }}
ATOMGIT_TOKEN: ${{ secrets.ATOMGIT_TOKEN }}
shell: bash
run: |
set -euo pipefail
if [ -z "${ATOMGIT_GIT_OWNER}" ] || [ -z "${ATOMGIT_GIT_REPO}" ] || [ -z "${ATOMGIT_GIT_USERNAME}" ] || [ -z "${ATOMGIT_TOKEN}" ]; then
echo "缺少 AtomGit Git 同步所需变量或密钥。"
exit 1
fi
REMOTE_URL="https://${ATOMGIT_GIT_USERNAME}:${ATOMGIT_TOKEN}@${ATOMGIT_GIT_HOST}/${ATOMGIT_GIT_OWNER}/${ATOMGIT_GIT_REPO}.git"
git remote add atomgit "$REMOTE_URL"
while read -r ref; do
branch="${ref#refs/remotes/origin/}"
git push atomgit "${ref}:refs/heads/${branch}"
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin | grep -v 'refs/remotes/origin/HEAD$')
git push atomgit 'refs/tags/*:refs/tags/*'
- name: 同步 AtomGit Release 与附件
if: steps.release_check.outputs.exists == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_OWNER: ${{ github.repository_owner }}
GITHUB_REPO: ${{ github.event.repository.name }}
RELEASE_TAG: ${{ steps.meta.outputs.tag }}
ATOMGIT_TOKEN: ${{ secrets.ATOMGIT_TOKEN }}
ATOMGIT_API_BASE: ${{ vars.ATOMGIT_API_BASE || 'https://api.atomgit.com' }}
ATOMGIT_OWNER: ${{ vars.ATOMGIT_REPO_OWNER || 'clickclaw'}}
ATOMGIT_REPO: ${{ vars.ATOMGIT_REPO_NAME || 'clickclaw'}}
run: node scripts/sync-atomgit-release.js
6. 同步脚本完整代码
文件:scripts/sync-atomgit-release.js
#!/usr/bin/env node
const { Readable } = require('node:stream')
function readCliArgs(argv) {
const options = {}
for (let index = 0; index < argv.length; index += 1) {
const current = argv[index]
if (current === '--tag') {
options.tag = argv[index + 1]
index += 1
continue
}
if (current.startsWith('--tag=')) {
options.tag = current.slice('--tag='.length)
}
}
return options
}
const cliArgs = readCliArgs(process.argv.slice(2))
if (cliArgs.tag) {
process.env.RELEASE_TAG = cliArgs.tag
}
if (!process.env.GITHUB_TOKEN && process.env.GH_TOKEN) {
process.env.GITHUB_TOKEN = process.env.GH_TOKEN
}
const dryRun = ['1', 'true', 'yes'].includes((process.env.DRY_RUN || '').trim().toLowerCase())
const requiredEnv = ['GITHUB_TOKEN', 'RELEASE_TAG']
if (!dryRun) {
requiredEnv.push('ATOMGIT_TOKEN')
}
for (const name of requiredEnv) {
if (!process.env[name]) {
console.error(`[atomgit-sync] missing env: ${name}`)
process.exit(1)
}
}
const GITHUB_API_BASE = 'https://api.github.com'
const ATOMGIT_API_BASE = (process.env.ATOMGIT_API_BASE || 'https://api.atomgit.com').replace(
/\/+$/,
''
)
const githubOwner = process.env.GITHUB_OWNER || 'clickclaw'
const githubRepo = process.env.GITHUB_REPO || 'clickclaw'
const releaseTag = process.env.RELEASE_TAG
const atomgitOwner = process.env.ATOMGIT_OWNER || 'clickclaw'
const atomgitRepo = process.env.ATOMGIT_REPO || 'clickclaw'
function logDryRun(message) {
console.log(`[atomgit-sync][dry-run] ${message}`)
}
function formatBytes(bytes) {
if (!Number.isFinite(bytes) || bytes < 0) return 'unknown'
const units = ['B', 'KB', 'MB', 'GB']
let value = bytes
let unitIndex = 0
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024
unitIndex += 1
}
return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`
}
function createProgressTracker(kind, name, totalBytes) {
let lastPercentBucket = -1
let lastLoggedBytes = 0
return (processedBytes) => {
if (!Number.isFinite(processedBytes)) return
if (Number.isFinite(totalBytes) && totalBytes > 0) {
const percent = Math.min(100, Math.floor((processedBytes / totalBytes) * 100))
const bucket = percent
if (bucket > lastPercentBucket) {
lastPercentBucket = bucket
console.log(
`[atomgit-sync] ${kind} ${name}: ${percent}% (${formatBytes(processedBytes)}/${formatBytes(totalBytes)})`
)
}
return
}
const step = 5 * 1024 * 1024
if (processedBytes - lastLoggedBytes >= step) {
lastLoggedBytes = processedBytes
console.log(`[atomgit-sync] ${kind} ${name}: ${formatBytes(processedBytes)}`)
}
}
}
async function requestJson(url, options = {}, { allow404 = false } = {}) {
const response = await fetch(url, options)
if (allow404 && response.status === 404) return null
if (!response.ok) {
const text = await response.text()
if (allow404) {
try {
const payload = JSON.parse(text)
const message = String(payload?.error_message || '')
if (message.includes('Release Not Found') || message.includes('404')) {
return null
}
} catch {
// ignore json parse error and throw original response error
}
}
throw new Error(`${options.method || 'GET'} ${url} failed: ${response.status} ${text}`)
}
if (response.status === 204) return null
return response.json()
}
function githubHeaders(extra = {}) {
return {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
'X-GitHub-Api-Version': '2022-11-28',
...extra,
}
}
function atomgitHeaders(extra = {}) {
return {
Accept: 'application/json',
Authorization: `Bearer ${process.env.ATOMGIT_TOKEN}`,
'PRIVATE-TOKEN': process.env.ATOMGIT_TOKEN,
...extra,
}
}
function buildAtomGitApiUrl(path, query = {}) {
const url = new URL(`${ATOMGIT_API_BASE}${path}`)
const accessToken = process.env.ATOMGIT_TOKEN
if (accessToken) {
url.searchParams.set('access_token', accessToken)
}
for (const [key, value] of Object.entries(query)) {
if (value == null || value === '') continue
url.searchParams.set(key, String(value))
}
return url.toString()
}
async function getGithubRelease() {
const url = `${GITHUB_API_BASE}/repos/${encodeURIComponent(githubOwner)}/${encodeURIComponent(githubRepo)}/releases/tags/${encodeURIComponent(releaseTag)}`
return requestJson(url, { headers: githubHeaders() })
}
async function getAtomGitRelease() {
if (dryRun) {
logDryRun(`would query AtomGit release by tag: ${releaseTag}`)
return null
}
const url = buildAtomGitApiUrl(
`/api/v5/repos/${encodeURIComponent(atomgitOwner)}/${encodeURIComponent(atomgitRepo)}/releases/${encodeURIComponent(releaseTag)}`
)
return requestJson(url, { headers: atomgitHeaders() }, { allow404: true })
}
function buildReleasePayload(release) {
return {
tag_name: release.tag_name,
target_commitish: release.target_commitish,
name: release.name,
body: release.body || '',
draft: Boolean(release.draft),
prerelease: Boolean(release.prerelease),
}
}
async function createOrUpdateAtomGitRelease(githubRelease) {
const existing = await getAtomGitRelease()
const payload = buildReleasePayload(githubRelease)
if (dryRun) {
logDryRun(
`${existing ? 'would update' : 'would create'} AtomGit release ${releaseTag} with name "${payload.name || ''}"`
)
return {
tag_name: payload.tag_name,
name: payload.name,
body: payload.body,
assets: [],
}
}
if (!existing) {
const url = buildAtomGitApiUrl(
`/api/v5/repos/${encodeURIComponent(atomgitOwner)}/${encodeURIComponent(atomgitRepo)}/releases`
)
const created = await requestJson(url, {
method: 'POST',
headers: atomgitHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
})
console.log(`[atomgit-sync] created release ${releaseTag}`)
return created
}
const url = buildAtomGitApiUrl(
`/api/v5/repos/${encodeURIComponent(atomgitOwner)}/${encodeURIComponent(atomgitRepo)}/releases/${encodeURIComponent(releaseTag)}`
)
const updated = await requestJson(url, {
method: 'PATCH',
headers: atomgitHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload),
})
console.log(`[atomgit-sync] updated release ${releaseTag}`)
return updated
}
function readUploadUrlPayload(payload) {
if (!payload) return null
if (typeof payload === 'string') return payload
if (typeof payload.upload_url === 'string') return payload.upload_url
if (typeof payload.url === 'string') return payload.url
if (typeof payload.href === 'string') return payload.href
return null
}
function normalizeUploadHeaders(headers) {
if (!headers || typeof headers !== 'object') return {}
const normalized = {}
for (const [key, value] of Object.entries(headers)) {
if (value == null) continue
normalized[key] = String(value)
}
return normalized
}
async function getAtomGitUploadUrl(fileName) {
if (dryRun) {
logDryRun(`would request AtomGit upload url for release ${releaseTag} and file ${fileName}`)
return {
url: 'https://example.invalid/upload',
headers: { 'Content-Type': 'application/octet-stream' },
}
}
const url = buildAtomGitApiUrl(
`/api/v5/repos/${encodeURIComponent(atomgitOwner)}/${encodeURIComponent(atomgitRepo)}/releases/${encodeURIComponent(releaseTag)}/upload_url`,
{ file_name: fileName }
)
const payload = await requestJson(url, { headers: atomgitHeaders() })
const uploadUrl = readUploadUrlPayload(payload)
if (!uploadUrl) {
throw new Error(`unexpected AtomGit upload_url payload: ${JSON.stringify(payload)}`)
}
return {
url: uploadUrl,
headers: normalizeUploadHeaders(payload.headers),
}
}
async function downloadGithubAsset(asset) {
const response = await fetch(asset.url, {
headers: githubHeaders({ Accept: 'application/octet-stream' }),
redirect: 'follow',
})
if (!response.ok) {
const text = await response.text()
throw new Error(`download GitHub asset ${asset.name} failed: ${response.status} ${text}`)
}
const totalBytes = Number(response.headers.get('content-length') || asset.size || 0)
const reportProgress = createProgressTracker('download', asset.name, totalBytes)
if (!response.body) {
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
reportProgress(buffer.byteLength)
return buffer
}
const reader = response.body.getReader()
const chunks = []
let processedBytes = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = Buffer.from(value)
chunks.push(chunk)
processedBytes += chunk.byteLength
reportProgress(processedBytes)
}
if (
processedBytes > 0 &&
(!Number.isFinite(totalBytes) || totalBytes <= 0 || processedBytes !== totalBytes)
) {
reportProgress(processedBytes)
}
return Buffer.concat(chunks)
}
async function uploadAtomGitAsset(uploadTarget, asset, buffer) {
if (dryRun) {
logDryRun(
`would upload asset ${asset.name} (${buffer.byteLength} bytes) to ${uploadTarget.url}`
)
return
}
const reportProgress = createProgressTracker('upload', asset.name, buffer.byteLength)
const baseHeaders = {
...uploadTarget.headers,
'Content-Length': String(buffer.byteLength),
}
if (!baseHeaders['Content-Type'] && !baseHeaders['content-type']) {
baseHeaders['Content-Type'] = asset.content_type || 'application/octet-stream'
}
let processedBytes = 0
const stream = Readable.from(
(function* generateChunks() {
const chunkSize = 1024 * 1024
for (let offset = 0; offset < buffer.byteLength; offset += chunkSize) {
const chunk = buffer.subarray(offset, Math.min(offset + chunkSize, buffer.byteLength))
processedBytes += chunk.byteLength
reportProgress(processedBytes)
yield chunk
}
})()
)
const response = await fetch(uploadTarget.url, {
method: 'PUT',
headers: baseHeaders,
body: stream,
duplex: 'half',
})
if (response.ok || response.status === 409) {
if (response.status === 409) {
console.log(`[atomgit-sync] asset exists, skipped: ${asset.name}`)
} else {
console.log(`[atomgit-sync] uploaded asset: ${asset.name}`)
}
return
}
const text = await response.text()
throw new Error(`PUT upload failed for ${asset.name}: ${response.status} ${text}`)
}
function listExistingAssetNames(release) {
if (!release || !Array.isArray(release.assets)) return new Set()
return new Set(release.assets.map((asset) => asset?.name).filter(Boolean))
}
async function syncAssets(githubRelease, atomgitRelease) {
if (!Array.isArray(githubRelease.assets) || githubRelease.assets.length === 0) {
console.log('[atomgit-sync] no GitHub release assets, skip upload')
return
}
const existingAssetNames = listExistingAssetNames(atomgitRelease)
for (const asset of githubRelease.assets) {
if (existingAssetNames.has(asset.name)) {
console.log(`[atomgit-sync] asset already present, skipped: ${asset.name}`)
continue
}
const uploadTarget = await getAtomGitUploadUrl(asset.name)
if (dryRun) {
logDryRun(
`would sync asset ${asset.name} (${asset.size ?? 'unknown'} bytes) to ${uploadTarget.url}`
)
continue
}
console.log(`[atomgit-sync] downloading asset: ${asset.name}`)
const buffer = await downloadGithubAsset(asset)
await uploadAtomGitAsset(uploadTarget, asset, buffer)
}
}
async function main() {
if (dryRun) {
logDryRun(`starting sync preview for ${githubOwner}/${githubRepo}@${releaseTag}`)
}
const githubRelease = await getGithubRelease()
console.log(`[atomgit-sync] loaded GitHub release ${githubRelease.tag_name}`)
const atomgitRelease = await createOrUpdateAtomGitRelease(githubRelease)
await syncAssets(githubRelease, atomgitRelease)
console.log(`[atomgit-sync] sync completed for ${releaseTag}`)
}
main().catch((error) => {
console.error(`[atomgit-sync] ${error.stack || error.message}`)
process.exit(1)
})
7. 首次上线操作步骤(推荐)
- 在 AtomGit 创建空仓库
- 配置 GitHub Secrets / Variables
- 提交上述两个文件
- 先在 GitHub 上确认已有一个测试 Release(例如
v0.0.1-test) - 手动触发
同步 AtomGit 发布,输入tag=v0.0.1-test - 查看日志并去 AtomGit 验证结果

图:通过 Run workflow 手动补偿触发,输入目标 tag。
8. 验证清单(上线必做)
一次同步成功后,至少检查:
- AtomGit 是否出现所有分支(或核心分支)
- Tag 是否可见
- Release 是否存在且标题/正文正确
- 附件是否完整且可下载
- 日志是否出现
sync completed for <tag>

9. 上线效果与收益
这套流程上线后,收益通常体现在:
- 发布一致性提升:GitHub 与 AtomGit 的 Tag/Release/附件更一致
- 分发体验提升:国内用户可优先从 AtomGit 下载安装包
- 人工成本下降:发版后不再手工重复上传附件
- 事故恢复更快:失败时可按 tag 手动补偿重放
- 过程更可控:日志可追踪,问题定位更直接
10. 常见问题排查
10.1 提示 GitHub Release xxx 不存在
原因:传入 tag 没有对应 GitHub Release。
处理:
- 先在 GitHub 创建该 tag 的 Release
- 再手动触发同步
10.2 提示缺少 AtomGit 变量或密钥
原因:Secrets / Variables 未配置完整。
处理:检查:
ATOMGIT_TOKENATOMGIT_GIT_USERNAMEATOMGIT_REPO_OWNERATOMGIT_REPO_NAME
10.3 分支推送认证失败
原因:Git HTTPS 认证不通过。
处理:
ATOMGIT_GIT_USERNAME是否正确。它通常就是你 AtomGit 个人主页链接中的 path。 例如个人主页是 https://atomgit.com/qq_15502821,则 ATOMGIT_GIT_USERNAME=qq_15502821Token令牌是否过期或权限不足
10.4 附件上传慢或不稳定
处理:
- 保留 workflow 的 hosts 代理解析步骤
- 失败后使用手动触发按 tag 补偿
- 大附件场景优先观察日志中的 download/upload 进度
10.5 重跑任务会不会重复上传
不会。脚本按附件文件名去重,AtomGit 已有同名附件会跳过。
11. 运维建议
- 建议把同步工作流与发布工作流解耦,便于独立重试
- 建议保留手动触发入口,作为稳定补偿通道
- 建议固定 Node 版本(示例为 22)避免环境漂移
- 建议在发布公告中统一给 AtomGit Release 链接,减少用户路径分叉
12. ClickClaw 实例说明
若你是基于 ClickClaw 方案落地:
- 生产 workflow:
.github/workflows/sync-atomgit-release.yml - 生产脚本:
scripts/sync-atomgit-release.js
你可以直接使用本教程代码,也可以根据自己的上游发布流程调整 workflow_run.workflows 名称。
昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链
更多推荐

所有评论(0)