name: Build Tutorial # 主仓库 gitea.idealjs.com/idealjs/stack 的 CI:打包新教程站。 # 构建环境全部封装在 apps/tutorial/Dockerfile(node24-alpine + yarn4 + npmmirror), # runner 用 buildah 无 daemon 执行——不依赖 runner 宿主的 node/docker,也不碰 github.com。 # 源码:gitea API tarball(同机房) # 产物:滚动 release「ci-dist」资产(node 取自镜像挂载层,原生 fetch 上传) on: push: branches: [main] pull_request: workflow_dispatch: env: YARN_NPM_REGISTRY_SERVER: https://registry.npmmirror.com API: https://gitea.idealjs.com/api/v1/repos/idealjs/stack jobs: build: name: Build @stack/tutorial (buildah) runs-on: ubuntu-latest steps: - name: 拉源码(gitea tarball) run: | wget -q "$API/archive/$GITHUB_SHA.tar.gz" -O /tmp/src.tgz tar -xzf /tmp/src.tgz --strip-components=1 ls apps/tutorial/Dockerfile - name: 装 buildah(runner 无 docker;buildah 无 daemon 即可构建) run: | command -v buildah >/dev/null && exit 0 # 服务器在阿里云——apk 走阿里镜像源(dl-cdn 直连极慢) sed -i 's#https://dl-cdn.alpinelinux.org#https://mirrors.aliyun.com#g' /etc/apk/repositories apk add --no-cache buildah - name: buildah 登录阿里云镜像仓(基座为私有 idealjs/node) env: ALIYUN_AUTH: ${{ secrets.ALIYUN_AUTH }} run: | echo "$ALIYUN_AUTH" | buildah login --username "${ALIYUN_AUTH%%:*}" --password-stdin registry.cn-hangzhou.aliyuncs.com - name: buildah 构建镜像(环境封装在 Dockerfile) run: | buildah build --storage-driver=vfs -f apps/tutorial/Dockerfile -t stack-tutorial:ci . - name: 取产物并上传 release(node 取自镜像挂载层) env: TOK: ${{ secrets.CI_TOKEN || github.token }} run: | CID=$(buildah from localhost/stack-tutorial:ci) MNT=$(buildah mount "$CID") ls "$MNT/repo/apps/tutorial/dist/index.html" tar -czf /tmp/tutorial-dist.tar.gz -C "$MNT/repo/apps/tutorial/dist" . export LD_LIBRARY_PATH="$MNT/usr/lib" cat > /tmp/upload.mjs <<'JS' const API = process.env.API const H = { Authorization: `token ${process.env.TOK}` } const j = async (u, opt) => { const r = await fetch(API + u, { ...opt, headers: { ...H, ...(opt?.headers || {}) } }); return { ok: r.ok, status: r.status, data: r.status === 204 ? null : await r.json().catch(() => null) } } let rid let r = await j('/releases/tags/ci-dist') if (r.ok) rid = r.data.id else { r = await j('/releases', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tag_name: 'ci-dist', name: 'CI 产物(滚动更新)', body: '每次构建的最新 dist。', draft: false, prerelease: true }) }) if (r.ok) rid = r.data.id } if (!rid) { console.log(`::warning::release 不可用(${r.status})——构建已成功,产物留在 runner`); process.exit(0) } for (const a of (await j(`/releases/${rid}/assets`)).data || []) await j(`/releases/${rid}/assets/${a.id}`, { method: 'DELETE' }) const fd = new FormData() fd.append('attachment', new Blob([await (await import('node:fs/promises')).readFile('/tmp/tutorial-dist.tar.gz')]), 'tutorial-dist.tar.gz') const up = await fetch(`${API}/releases/${rid}/assets?name=tutorial-dist.tar.gz`, { method: 'POST', headers: H, body: fd }) console.log(up.ok ? `产物下载:${(await up.json()).browser_download_url}` : `上传失败 ${up.status}`) JS "$MNT/usr/local/bin/node" /tmp/upload.mjs buildah unmount "$CID" >/dev/null 2>&1 || true buildah rm "$CID" >/dev/null 2>&1 || true