From 92bd420faf2a1a975a076605ea21b8da0d599f2a Mon Sep 17 00:00:00 2001 From: Wenl4ng <15861868579@163.com> Date: Tue, 13 Oct 2026 11:11:35 +0800 Subject: [PATCH 1/9] ci: build and push multi-arch runner images to SWR on GitHub Actions - replace Jenkins docker buildx pipeline with GitHub Actions - matrix over runner versions from .github/runner-versions.yaml - qemu + buildx multi-arch (linux/amd64, linux/arm64), provenance disabled - tag images - on release, -- otherwise - login to SWR cn-southwest-2 via SWR_AK / SWR_LOGIN_KEY secrets - gha build cache --- .github/workflows/build-image.yaml | 72 +++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 2f41db9a..d3ab13ab 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -1,14 +1,26 @@ -name: Build runner image +name: Build & Push runner image -# 用 runner-versions.yaml 里的每个底座版本构建自有 runner 镜像。 +# 用 runner-versions.yaml 里的每个底座版本构建自有 runner 镜像并推送到 SWR。 +# - 打 tag(v*)时:镜像 tag = -(不可变,生产引用) +# - 推到 main 时:镜像 tag = -main-<短sha>(开发验证用) # 底座版本升级只改 .github/runner-versions.yaml,不改 Dockerfile。 -# 默认只构建(不推送);配置好 registry 后取消 push 相关步骤的注释。 on: push: tags: - 'v*' + branches: + - main workflow_dispatch: +env: + REGISTRY: swr.cn-southwest-2.myhuaweicloud.com + IMAGE_NAME: swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/runner + BASE_REGISTRY: ghcr.io + +concurrency: + group: build-image-${{ github.ref }} + cancel-in-progress: true + jobs: versions: runs-on: ubuntu-latest @@ -19,8 +31,7 @@ jobs: - id: read run: | matrix=$(grep -E '^\s*-\s*"' .github/runner-versions.yaml \ - | sed 's/.*"\(.*\)".*/\1/' \ - | jq -R -s -c 'split("\n")[:-1]') + | sed 's/.*"\(.*\)".*/\1/' | jq -R -s -c 'split("\n")[:-1]') echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" build: @@ -32,20 +43,39 @@ jobs: runner_version: ${{ fromJson(needs.versions.outputs.matrix) }} steps: - uses: actions/checkout@v5 - - name: Build image for runner ${{ matrix.runner_version }} + + - name: Compute image tag + id: tag run: | - docker build \ - --build-arg RUNNER_VERSION=${{ matrix.runner_version }} \ - -t runner-container-hooks:${{ matrix.runner_version }}-${{ github.ref_name }} \ - . - # - name: Log in to registry - # uses: docker/login-action@v3 - # with: - # registry: ${{ vars.REGISTRY }} - # username: ${{ secrets.REGISTRY_USERNAME }} - # password: ${{ secrets.REGISTRY_PASSWORD }} - # - name: Push image - # run: | - # docker tag runner-container-hooks:${{ matrix.runner_version }}-${{ github.ref_name }} \ - # ${{ vars.REGISTRY }}/runner:${{ matrix.runner_version }}-${{ github.ref_name }} - # docker push ${{ vars.REGISTRY }}/runner:${{ matrix.runner_version }}-${{ github.ref_name }} + short=$(echo "$GITHUB_SHA" | cut -c1-6) + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + tag="${{ matrix.runner_version }}-${GITHUB_REF_NAME}" + else + tag="${{ matrix.runner_version }}-${GITHUB_REF_NAME}-${short}" + fi + echo "tag=${tag}" >> "$GITHUB_OUTPUT" + echo "resolved tag: ${tag}" + + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + + - name: Login to SWR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: cn-southwest-2@${{ secrets.SWR_AK }} + password: ${{ secrets.SWR_LOGIN_KEY }} + + - name: Build and push (linux/amd64, linux/arm64) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + provenance: false + build-args: | + RUNNER_VERSION=${{ matrix.runner_version }} + BASE_REGISTRY=${{ env.BASE_REGISTRY }} + tags: ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} + cache-from: type=gha,scope=${{ matrix.runner_version }} + cache-to: type=gha,mode=max,scope=${{ matrix.runner_version }} From ad56b81d3191634e5c1c64c32fe3de7481142ada Mon Sep 17 00:00:00 2001 From: Wenl4ng <15861868579@163.com> Date: Tue, 13 Oct 2026 11:40:20 +0800 Subject: [PATCH 2/9] ci: rename SWR secrets to SWR_USERNAME / SWR_PASSWORD --- .github/workflows/build-image.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index d3ab13ab..3b01cc3b 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -63,8 +63,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} - username: cn-southwest-2@${{ secrets.SWR_AK }} - password: ${{ secrets.SWR_LOGIN_KEY }} + username: cn-southwest-2@${{ secrets.SWR_USERNAME }} + password: ${{ secrets.SWR_PASSWORD }} - name: Build and push (linux/amd64, linux/arm64) uses: docker/build-push-action@v6 From b2a3b001f10c0a32ed91f5d90cbe6679c335ca04 Mon Sep 17 00:00:00 2001 From: Wenl4ng <15861868579@163.com> Date: Tue, 13 Oct 2026 11:42:13 +0800 Subject: [PATCH 3/9] ci: use SWR_USERNAME as full username (secret already includes region prefix) --- .github/workflows/build-image.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 3b01cc3b..5fafaba4 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -63,7 +63,7 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} - username: cn-southwest-2@${{ secrets.SWR_USERNAME }} + username: ${{ secrets.SWR_USERNAME }} password: ${{ secrets.SWR_PASSWORD }} - name: Build and push (linux/amd64, linux/arm64) From c5e915a61e277060e6b2bb4acc911c9fa854dd00 Mon Sep 17 00:00:00 2001 From: Wenl4ng <15861868579@163.com> Date: Tue, 13 Oct 2026 11:45:13 +0800 Subject: [PATCH 4/9] ci: build test images from branch pushes (tag = --) - trigger on all branches (except dependabot) as well as release tags - branch builds use mutable -- tags - sanitize branch name for docker tag; cap length - document release vs test image tagging in docs/RELEASE.md --- .github/workflows/build-image.yaml | 15 ++++++++++----- docs/RELEASE.md | 12 +++++++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 5fafaba4..6b01ec9b 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -2,14 +2,15 @@ name: Build & Push runner image # 用 runner-versions.yaml 里的每个底座版本构建自有 runner 镜像并推送到 SWR。 # - 打 tag(v*)时:镜像 tag = -(不可变,生产引用) -# - 推到 main 时:镜像 tag = -main-<短sha>(开发验证用) +# - 推分支时:镜像 tag = --<短sha>(测试/开发用,可变) # 底座版本升级只改 .github/runner-versions.yaml,不改 Dockerfile。 on: push: tags: - 'v*' branches: - - main + - '**' + - '!dependabot/**' workflow_dispatch: env: @@ -47,12 +48,16 @@ jobs: - name: Compute image tag id: tag run: | - short=$(echo "$GITHUB_SHA" | cut -c1-6) + short=$(printf '%s' "$GITHUB_SHA" | cut -c1-6) if [ "${GITHUB_REF_TYPE}" = "tag" ]; then - tag="${{ matrix.runner_version }}-${GITHUB_REF_NAME}" + suffix="${GITHUB_REF_NAME}" else - tag="${{ matrix.runner_version }}-${GITHUB_REF_NAME}-${short}" + # 分支名里的 / 等非法字符替换为 -,才能作为 docker tag + branch=$(printf '%s' "${GITHUB_REF_NAME}" | sed -E 's#[^A-Za-z0-9_.-]#-#g') + suffix="${branch}-${short}" fi + tag="${{ matrix.runner_version }}-${suffix}" + tag="${tag:0:120}" echo "tag=${tag}" >> "$GITHUB_OUTPUT" echo "resolved tag: ${tag}" diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 792a21a5..200a10dd 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -42,9 +42,19 @@ git merge vX.Y.0 # 合并上游新版本 tag ## 自有 runner 镜像 +由 GitHub Actions(`.github/workflows/build-image.yaml`)构建并推送到 SWR(替换原 Jenkins 流水线)。 + - 底座版本集中在 `.github/runner-versions.yaml`,升级 runner 只改此文件。 - `Dockerfile` 通过 `--build-arg RUNNER_VERSION=` 接收底座版本。 -- 镜像 tag 编码两个维度:`-`,例如 `2.334.0-v0.8.1-ascend.1`。 +- 镜像 tag 编码两个维度: + +| 场景 | 触发 | 镜像 tag | 可变性 | +|---|---|---|---| +| 生产 | 打 tag `vX.Y.Z-ascend.N` | `-vX.Y.Z-ascend.N` | 不可变 | +| 测试 | 推分支 / 手动 dispatch | `--<短sha>` | 可变 | + +- 测试镜像用分支名 + 短 sha 标识,不打 tag、随时可弃;生产只引用不可变的 tag 镜像。 +- 需要 secrets:`SWR_USERNAME`(含 region 前缀的完整用户名)、`SWR_PASSWORD`;镜像名与仓库见 workflow 的 `IMAGE_NAME`。 ## 回滚 From bee0986fbcc41c114c81d671b6a0384e787a4abb Mon Sep 17 00:00:00 2001 From: Wenl4ng <15861868579@163.com> Date: Tue, 13 Oct 2026 11:48:52 +0800 Subject: [PATCH 5/9] style: fix prettier formatting (7 files) so CI format-check passes --- packages/docker/src/utils/index.spec.ts | 7 ++- packages/k8s/src/hooks/prepare-job.ts | 7 +-- packages/k8s/src/hooks/run-script-step.ts | 12 ++++- packages/k8s/src/k8s/utils/index.spec.ts | 39 +++++++++++---- .../tests/exec-pod-step-with-output-test.ts | 48 +++++++++++++++++-- .../run-container-step-terminated-test.ts | 35 +++++++++----- .../k8s/tests/run-script-step-unit-test.ts | 7 +-- 7 files changed, 119 insertions(+), 36 deletions(-) diff --git a/packages/docker/src/utils/index.spec.ts b/packages/docker/src/utils/index.spec.ts index 9bb5070e..88e43966 100644 --- a/packages/docker/src/utils/index.spec.ts +++ b/packages/docker/src/utils/index.spec.ts @@ -1,4 +1,9 @@ -import { optionsWithDockerEnvs, sanitize, fixArgs, checkEnvironment } from './index' +import { + optionsWithDockerEnvs, + sanitize, + fixArgs, + checkEnvironment +} from './index' describe('sanitize', () => { it('strips non-alphanumeric chars except underscore', () => { diff --git a/packages/k8s/src/hooks/prepare-job.ts b/packages/k8s/src/hooks/prepare-job.ts index d40b7019..19b90082 100644 --- a/packages/k8s/src/hooks/prepare-job.ts +++ b/packages/k8s/src/hooks/prepare-job.ts @@ -104,9 +104,10 @@ export async function prepareJob( // The boundary may be '"\nHeaders:' (real newline) or the literal // string ends before Headers — use the last '"' before 'Headers:' as fallback const headersIdx = raw.indexOf('Headers:') - const bodyEnd = headersIdx !== -1 - ? raw.lastIndexOf('"', headersIdx) // last " before Headers: - : raw.indexOf('"\nHeaders:') + const bodyEnd = + headersIdx !== -1 + ? raw.lastIndexOf('"', headersIdx) // last " before Headers: + : raw.indexOf('"\nHeaders:') if (bodyStart !== -1 && bodyEnd !== -1 && bodyEnd > bodyStart) { // Body content is a JSON string literal (without surrounding quotes). // Wrap it in quotes and JSON.parse to properly unescape \" \\ \n \t etc. diff --git a/packages/k8s/src/hooks/run-script-step.ts b/packages/k8s/src/hooks/run-script-step.ts index 2867064d..a08aa323 100644 --- a/packages/k8s/src/hooks/run-script-step.ts +++ b/packages/k8s/src/hooks/run-script-step.ts @@ -2,7 +2,12 @@ import * as fs from 'fs' import * as core from '@actions/core' import { RunScriptStepArgs } from 'hooklib' -import { execCpFromPod, execCpToPod, execPodStep, execPodStepWithOutput } from '../k8s' +import { + execCpFromPod, + execCpToPod, + execPodStep, + execPodStepWithOutput +} from '../k8s' import { writeRunScript } from '../k8s/utils' import { JOB_CONTAINER_NAME } from './constants' import { dirname } from 'path' @@ -84,7 +89,10 @@ export async function runScriptStep( } } catch (err) { core.debug(`execPodStep failed: ${JSON.stringify(err)}`) - if (err instanceof Error && err.message.startsWith('failed to run script step')) { + if ( + err instanceof Error && + err.message.startsWith('failed to run script step') + ) { throw err } const message = (err as any)?.response?.body?.message || err diff --git a/packages/k8s/src/k8s/utils/index.spec.ts b/packages/k8s/src/k8s/utils/index.spec.ts index 568fb3cc..62413958 100644 --- a/packages/k8s/src/k8s/utils/index.spec.ts +++ b/packages/k8s/src/k8s/utils/index.spec.ts @@ -33,13 +33,17 @@ describe('generateContainerName', () => { expect(generateContainerName('postgres:latest')).toBe('postgres') expect(generateContainerName('postgres')).toBe('postgres') expect( - generateContainerName('public.ecr.aws/url/with/multiple/slashes/postgres:latest') + generateContainerName( + 'public.ecr.aws/url/with/multiple/slashes/postgres:latest' + ) ).toBe('postgres') }) it('throws on invalid image string', () => { expect(() => generateContainerName(':latest')).toThrow() - expect(() => generateContainerName('localstack/localstack/:latest')).toThrow() + expect(() => + generateContainerName('localstack/localstack/:latest') + ).toThrow() }) }) @@ -144,7 +148,10 @@ describe('writeRunScript', () => { }) it('returns containerPath and runnerPath', () => { - const { containerPath, runnerPath } = writeRunScript('/work', 'sh', ['-e', 'script.sh']) + const { containerPath, runnerPath } = writeRunScript('/work', 'sh', [ + '-e', + 'script.sh' + ]) expect(containerPath).toMatch(/\/__w\/_temp\/.*\.sh/) expect(runnerPath).toContain(tmpDir) expect(fs.existsSync(runnerPath)).toBe(true) @@ -175,7 +182,7 @@ describe('writeRunScript', () => { it('throws if env key contains "$"', () => { expect(() => - writeRunScript('/work', 'sh', [], [], { 'BAD$KEY': 'val' }) + writeRunScript('/work', 'sh', [], [], { BAD$KEY: 'val' }) ).toThrow() }) }) @@ -205,9 +212,9 @@ describe('writeContainerStepScript', () => { }) it('throws on invalid working directory', () => { - expect(() => - writeContainerStepScript(tmpDir, 'tooshort', 'sh') - ).toThrow('Invalid working directory') + expect(() => writeContainerStepScript(tmpDir, 'tooshort', 'sh')).toThrow( + 'Invalid working directory' + ) }) it('throws if env key contains invalid chars', () => { @@ -234,7 +241,11 @@ describe('prepareJobScript', () => { it('returns paths and file exists', () => { const { containerPath, runnerPath } = prepareJobScript([ - { sourceVolumePath: '/src', targetVolumePath: '/mnt/data', readOnly: false } + { + sourceVolumePath: '/src', + targetVolumePath: '/mnt/data', + readOnly: false + } ]) expect(containerPath).toMatch(/\/__w\/_temp\/.*\.sh/) expect(fs.existsSync(runnerPath)).toBe(true) @@ -242,8 +253,16 @@ describe('prepareJobScript', () => { it('includes mkdir for each mount target', () => { const { runnerPath } = prepareJobScript([ - { sourceVolumePath: '/s1', targetVolumePath: '/mnt/vol1', readOnly: false }, - { sourceVolumePath: '/s2', targetVolumePath: '/mnt/vol2', readOnly: false } + { + sourceVolumePath: '/s1', + targetVolumePath: '/mnt/vol1', + readOnly: false + }, + { + sourceVolumePath: '/s2', + targetVolumePath: '/mnt/vol2', + readOnly: false + } ]) const content = fs.readFileSync(runnerPath, 'utf8') expect(content).toContain('/mnt/vol1') diff --git a/packages/k8s/tests/exec-pod-step-with-output-test.ts b/packages/k8s/tests/exec-pod-step-with-output-test.ts index 9f67bbdb..62e22c0b 100644 --- a/packages/k8s/tests/exec-pod-step-with-output-test.ts +++ b/packages/k8s/tests/exec-pod-step-with-output-test.ts @@ -24,7 +24,17 @@ describe('execPodStepWithOutput', () => { it('resolves with code 0 and captured stdout on success', async () => { execSpy.mockImplementation( - (_ns, _pod, _container, _cmd, stdout, _stderr, _stdin, _tty, callback) => { + ( + _ns, + _pod, + _container, + _cmd, + stdout, + _stderr, + _stdin, + _tty, + callback + ) => { stdout.write('hello from script\n') stdout.write('second line\n') callback({ status: 'Success', code: 0 }) @@ -62,7 +72,17 @@ describe('execPodStepWithOutput', () => { it('resolves with exit code from Failure status callback', async () => { execSpy.mockImplementation( - (_ns, _pod, _container, _cmd, _stdout, stderr, _stdin, _tty, callback) => { + ( + _ns, + _pod, + _container, + _cmd, + _stdout, + stderr, + _stdin, + _tty, + callback + ) => { stderr.write('error output\n') callback({ status: 'Failure', @@ -91,7 +111,17 @@ describe('execPodStepWithOutput', () => { it('rejects on Failure callback with no exit code in message', async () => { execSpy.mockImplementation( - (_ns, _pod, _container, _cmd, _stdout, _stderr, _stdin, _tty, callback) => { + ( + _ns, + _pod, + _container, + _cmd, + _stdout, + _stderr, + _stdin, + _tty, + callback + ) => { callback({ status: 'Failure', message: 'container not found' }) return Promise.resolve() } @@ -103,7 +133,17 @@ describe('execPodStepWithOutput', () => { it('retains only the last tailLines lines in the output buffer', async () => { execSpy.mockImplementation( - (_ns, _pod, _container, _cmd, stdout, _stderr, _stdin, _tty, callback) => { + ( + _ns, + _pod, + _container, + _cmd, + stdout, + _stderr, + _stdin, + _tty, + callback + ) => { for (let i = 0; i < 30; i++) { stdout.write(`line ${i}\n`) } diff --git a/packages/k8s/tests/run-container-step-terminated-test.ts b/packages/k8s/tests/run-container-step-terminated-test.ts index 8111a895..04e3038c 100644 --- a/packages/k8s/tests/run-container-step-terminated-test.ts +++ b/packages/k8s/tests/run-container-step-terminated-test.ts @@ -66,17 +66,22 @@ describe('runContainerStep terminated error detection', () => { waitForPodPhasesSpy = jest .spyOn(k8sModule, 'waitForPodPhases') - .mockRejectedValue(new Error('Pod test-step-pod has unrecoverable errors')) + .mockRejectedValue( + new Error('Pod test-step-pod has unrecoverable errors') + ) - getPodByNameSpy = jest - .spyOn(k8sModule, 'getPodByName') + getPodByNameSpy = jest.spyOn(k8sModule, 'getPodByName') - getContainerTerminatedErrorsSpy = jest - .spyOn(k8sModule, 'getContainerTerminatedErrors') + getContainerTerminatedErrorsSpy = jest.spyOn( + k8sModule, + 'getContainerTerminatedErrors' + ) describePodFailureSpy = jest .spyOn(k8sModule, 'describePodFailure') - .mockResolvedValue('Pod status: Failed\nContainer details:\n ✗ container "job" terminated: OOMKilled (exit code 137)') + .mockResolvedValue( + 'Pod status: Failed\nContainer details:\n ✗ container "job" terminated: OOMKilled (exit code 137)' + ) deletePodSpy = jest .spyOn(k8sModule, 'deletePod') @@ -96,7 +101,12 @@ describe('runContainerStep terminated error detection', () => { it('detects OOMKilled and logs describePodFailure output', async () => { getPodByNameSpy.mockResolvedValue( buildPodWithTerminated([ - terminatedContainer('job', 'OOMKilled', 137, 'The node was low on resource: memory') + terminatedContainer( + 'job', + 'OOMKilled', + 137, + 'The node was low on resource: memory' + ) ]) ) getContainerTerminatedErrorsSpy.mockReturnValue([ @@ -115,9 +125,7 @@ describe('runContainerStep terminated error detection', () => { it('detects Error (exit non-zero) and logs describePodFailure output', async () => { getPodByNameSpy.mockResolvedValue( - buildPodWithTerminated([ - terminatedContainer('job', 'Error', 1) - ]) + buildPodWithTerminated([terminatedContainer('job', 'Error', 1)]) ) getContainerTerminatedErrorsSpy.mockReturnValue([ ' ✗ container "job": Error (exit code 1)' @@ -136,7 +144,12 @@ describe('runContainerStep terminated error detection', () => { it('detects FailedPostStartHookError and logs describePodFailure output', async () => { getPodByNameSpy.mockResolvedValue( buildPodWithTerminated([ - terminatedContainer('job', 'FailedPostStartHookError', 137, 'postStart hook failed') + terminatedContainer( + 'job', + 'FailedPostStartHookError', + 137, + 'postStart hook failed' + ) ]) ) getContainerTerminatedErrorsSpy.mockReturnValue([ diff --git a/packages/k8s/tests/run-script-step-unit-test.ts b/packages/k8s/tests/run-script-step-unit-test.ts index 48d95792..06b144bc 100644 --- a/packages/k8s/tests/run-script-step-unit-test.ts +++ b/packages/k8s/tests/run-script-step-unit-test.ts @@ -39,9 +39,7 @@ describe('runScriptStep error classification', () => { runnerPath: '/tmp/test-script.sh' }) - execPodStepSpy = jest - .spyOn(k8sModule, 'execPodStep') - .mockResolvedValue(0) + execPodStepSpy = jest.spyOn(k8sModule, 'execPodStep').mockResolvedValue(0) execCpToPodSpy = jest .spyOn(k8sModule, 'execCpToPod') @@ -51,8 +49,7 @@ describe('runScriptStep error classification', () => { .spyOn(k8sModule, 'execCpFromPod') .mockResolvedValue(undefined) - execPodStepWithOutputSpy = jest - .spyOn(k8sModule, 'execPodStepWithOutput') + execPodStepWithOutputSpy = jest.spyOn(k8sModule, 'execPodStepWithOutput') }) afterEach(() => { From b9a417c49435c9bf19d7407dee67b90895f242d4 Mon Sep 17 00:00:00 2001 From: Wenl4ng <15861868579@163.com> Date: Tue, 13 Oct 2026 14:26:22 +0800 Subject: [PATCH 6/9] ci: make runner image build manual-only (workflow_dispatch) - remove push (branch/tag) triggers to avoid wasted resources and image tag clutter - default runner_version = latest in runner-versions.yaml; image_tag defaults to -- - update docs/RELEASE.md --- .github/workflows/build-image.yaml | 81 ++++++++++++++---------------- docs/RELEASE.md | 17 ++++--- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 6b01ec9b..4783ce28 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -1,17 +1,19 @@ name: Build & Push runner image -# 用 runner-versions.yaml 里的每个底座版本构建自有 runner 镜像并推送到 SWR。 -# - 打 tag(v*)时:镜像 tag = -(不可变,生产引用) -# - 推分支时:镜像 tag = --<短sha>(测试/开发用,可变) -# 底座版本升级只改 .github/runner-versions.yaml,不改 Dockerfile。 +# 手动触发:构建并推送自有 runner 镜像到 SWR。 +# 不在 push 时自动跑(省资源、避免堆镜像 tag),也不创建 git tag。 +# 触发:Actions 页面 → Run workflow,可选填 runner_version / image_tag。 on: - push: - tags: - - 'v*' - branches: - - '**' - - '!dependabot/**' workflow_dispatch: + inputs: + runner_version: + description: 'runner base version (default: latest in .github/runner-versions.yaml)' + type: string + required: false + image_tag: + description: 'image tag (default: --)' + type: string + required: false env: REGISTRY: swr.cn-southwest-2.myhuaweicloud.com @@ -20,47 +22,40 @@ env: concurrency: group: build-image-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: - versions: + resolve: runs-on: ubuntu-latest outputs: - matrix: ${{ steps.read.outputs.matrix }} + runner_version: ${{ steps.r.outputs.runner_version }} + image_tag: ${{ steps.r.outputs.image_tag }} steps: - uses: actions/checkout@v5 - - id: read + - id: r run: | - matrix=$(grep -E '^\s*-\s*"' .github/runner-versions.yaml \ - | sed 's/.*"\(.*\)".*/\1/' | jq -R -s -c 'split("\n")[:-1]') - echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" + rv="${{ inputs.runner_version }}" + if [ -z "$rv" ]; then + rv=$(grep -E '^\s*-\s*"' .github/runner-versions.yaml \ + | sed 's/.*"\(.*\)".*/\1/' | sort -V | tail -1) + fi + echo "runner_version=${rv}" >> "$GITHUB_OUTPUT" + + it="${{ inputs.image_tag }}" + if [ -z "$it" ]; then + short=$(printf '%s' "$GITHUB_SHA" | cut -c1-6) + branch=$(printf '%s' "${GITHUB_REF_NAME}" | sed -E 's#[^A-Za-z0-9_.-]#-#g') + it="${rv}-${branch}-${short}" + fi + it="${it:0:120}" + echo "image_tag=${it}" >> "$GITHUB_OUTPUT" + echo "runner_version=${rv} image_tag=${it}" build: - needs: versions + needs: resolve runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - runner_version: ${{ fromJson(needs.versions.outputs.matrix) }} steps: - uses: actions/checkout@v5 - - - name: Compute image tag - id: tag - run: | - short=$(printf '%s' "$GITHUB_SHA" | cut -c1-6) - if [ "${GITHUB_REF_TYPE}" = "tag" ]; then - suffix="${GITHUB_REF_NAME}" - else - # 分支名里的 / 等非法字符替换为 -,才能作为 docker tag - branch=$(printf '%s' "${GITHUB_REF_NAME}" | sed -E 's#[^A-Za-z0-9_.-]#-#g') - suffix="${branch}-${short}" - fi - tag="${{ matrix.runner_version }}-${suffix}" - tag="${tag:0:120}" - echo "tag=${tag}" >> "$GITHUB_OUTPUT" - echo "resolved tag: ${tag}" - - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 @@ -79,8 +74,8 @@ jobs: push: true provenance: false build-args: | - RUNNER_VERSION=${{ matrix.runner_version }} + RUNNER_VERSION=${{ needs.resolve.outputs.runner_version }} BASE_REGISTRY=${{ env.BASE_REGISTRY }} - tags: ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - cache-from: type=gha,scope=${{ matrix.runner_version }} - cache-to: type=gha,mode=max,scope=${{ matrix.runner_version }} + tags: ${{ env.IMAGE_NAME }}:${{ needs.resolve.outputs.image_tag }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 200a10dd..1edb916d 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -42,18 +42,19 @@ git merge vX.Y.0 # 合并上游新版本 tag ## 自有 runner 镜像 -由 GitHub Actions(`.github/workflows/build-image.yaml`)构建并推送到 SWR(替换原 Jenkins 流水线)。 +由 GitHub Actions(`.github/workflows/build-image.yaml`)**手动触发**构建并推送到 SWR(替换原 Jenkins 流水线,不在 push 时自动跑,省资源)。 -- 底座版本集中在 `.github/runner-versions.yaml`,升级 runner 只改此文件。 +- 入口:Actions → **Build & Push runner image** → Run workflow(可选填 `runner_version`、`image_tag`)。 +- 底座版本集中在 `.github/runner-versions.yaml`,默认取**最新版**;手动运行时可指定。 - `Dockerfile` 通过 `--build-arg RUNNER_VERSION=` 接收底座版本。 -- 镜像 tag 编码两个维度: -| 场景 | 触发 | 镜像 tag | 可变性 | -|---|---|---|---| -| 生产 | 打 tag `vX.Y.Z-ascend.N` | `-vX.Y.Z-ascend.N` | 不可变 | -| 测试 | 推分支 / 手动 dispatch | `--<短sha>` | 可变 | +镜像 tag 约定: + +| 场景 | 镜像 tag | 说明 | +|---|---|---| +| 生产 | `-vX.Y.Z-ascend.N` | 手动运行并指定 `image_tag`,不可变 | +| 测试 | `--<短sha>` | 不填 `image_tag` 时的默认值,可变可弃 | -- 测试镜像用分支名 + 短 sha 标识,不打 tag、随时可弃;生产只引用不可变的 tag 镜像。 - 需要 secrets:`SWR_USERNAME`(含 region 前缀的完整用户名)、`SWR_PASSWORD`;镜像名与仓库见 workflow 的 `IMAGE_NAME`。 ## 回滚 From 7f87fe7afddda8b17ed988b1fd12337d142d1862 Mon Sep 17 00:00:00 2001 From: Wenl4ng <15861868579@163.com> Date: Tue, 13 Oct 2026 14:38:14 +0800 Subject: [PATCH 7/9] fix(lint): resolve ESLint errors in fork-added/changed files - add explicit return types (k8s/index.ts) - rename shadowed 'pod' in run-container-step.ts - drop unused callback param / use optional catch binding - remove unused imports and spy variables in tests - add eslint-disable for intentional unused signature param All files are fork-added or fork-modified; no upstream-only files touched. --- packages/k8s/src/hooks/run-container-step.ts | 4 ++-- packages/k8s/src/k8s/index.ts | 13 ++++++----- .../tests/exec-pod-step-with-output-test.ts | 23 +++++++++++++------ .../run-container-step-terminated-test.ts | 17 ++++---------- .../k8s/tests/run-script-step-unit-test.ts | 14 +++-------- 5 files changed, 33 insertions(+), 38 deletions(-) diff --git a/packages/k8s/src/hooks/run-container-step.ts b/packages/k8s/src/hooks/run-container-step.ts index 16b8853f..7156d1f8 100644 --- a/packages/k8s/src/hooks/run-container-step.ts +++ b/packages/k8s/src/hooks/run-container-step.ts @@ -142,8 +142,8 @@ export async function runContainerStep( } } catch (error) { try { - const pod = await getPodByName(podName) - const terminatedErrors = getContainerTerminatedErrors(pod) + const fetchedPod = await getPodByName(podName) + const terminatedErrors = getContainerTerminatedErrors(fetchedPod) if (terminatedErrors.length > 0) { const details = await describePodFailure(podName) core.error( diff --git a/packages/k8s/src/k8s/index.ts b/packages/k8s/src/k8s/index.ts index 52980457..d5da2256 100644 --- a/packages/k8s/src/k8s/index.ts +++ b/packages/k8s/src/k8s/index.ts @@ -300,7 +300,7 @@ export async function execPodStepWithOutput( // Ring buffer of the last N non-empty lines (cap to keep memory bounded). const buffer: string[] = [] - const push = (line: string) => { + const push = (line: string): void => { if (line.length === 0) return buffer.push(line) if (buffer.length > tailLines) buffer.shift() @@ -308,7 +308,7 @@ export async function execPodStepWithOutput( // Separate pending buffers per stream to prevent stdout/stderr interleaving. let pendingOut = '' - const ingestOut = (chunk: Buffer | string) => { + const ingestOut = (chunk: Buffer | string): void => { pendingOut += chunk.toString('utf8') const lines = pendingOut.split(/\r?\n/) pendingOut = lines.pop() ?? '' @@ -316,14 +316,14 @@ export async function execPodStepWithOutput( } let pendingErr = '' - const ingestErr = (chunk: Buffer | string) => { + const ingestErr = (chunk: Buffer | string): void => { pendingErr += chunk.toString('utf8') const lines = pendingErr.split(/\r?\n/) pendingErr = lines.pop() ?? '' for (const line of lines) push(line) } - const flushPending = () => { + const flushPending = (): void => { if (pendingOut) { push(pendingOut) pendingOut = '' @@ -682,7 +682,7 @@ export async function execCpFromPod( errStream, null, false, - async _status => { + async () => { if (errStream.size()) { reject( new Error( @@ -1493,6 +1493,7 @@ async function describePodWarningEvents(podName: string): Promise { // until the timeout, not terminate early. This function always returns [] as a // result, but is kept so checkUnrecoverableErrors compiles and the dedup logic // remains intact for future use. +// eslint-disable-next-line @typescript-eslint/no-unused-vars -- signature kept for callers; always returns [] export function getPodConditionErrors(_pod: k8s.V1Pod): string[] { return [] } @@ -1610,7 +1611,7 @@ export async function waitForPodPhases( try { await backOffManager.backOff() - } catch (error) { + } catch { // BackOffManager throws "backoff timeout" when maxTimeSeconds is exceeded. // Don't surface that bare message: collect diagnostics first so the user // can see WHY the pod never became ready. diff --git a/packages/k8s/tests/exec-pod-step-with-output-test.ts b/packages/k8s/tests/exec-pod-step-with-output-test.ts index 62e22c0b..b947fcfd 100644 --- a/packages/k8s/tests/exec-pod-step-with-output-test.ts +++ b/packages/k8s/tests/exec-pod-step-with-output-test.ts @@ -1,5 +1,4 @@ import * as k8s from '@kubernetes/client-node' -import * as core from '@actions/core' import { execPodStepWithOutput } from '../src/k8s' jest.mock('@actions/core', () => ({ @@ -24,7 +23,7 @@ describe('execPodStepWithOutput', () => { it('resolves with code 0 and captured stdout on success', async () => { execSpy.mockImplementation( - ( + async ( _ns, _pod, _container, @@ -53,7 +52,7 @@ describe('execPodStepWithOutput', () => { it('resolves with exit code when exec() promise rejects with non-zero exit message', async () => { // This is the k8s client v1.x path: exec() rejects directly, callback never fires. - execSpy.mockImplementation((_ns, _pod, _container, _cmd, stdout) => { + execSpy.mockImplementation(async (_ns, _pod, _container, _cmd, stdout) => { stdout.write('some output before fail\n') return Promise.reject( new Error( @@ -72,7 +71,7 @@ describe('execPodStepWithOutput', () => { it('resolves with exit code from Failure status callback', async () => { execSpy.mockImplementation( - ( + async ( _ns, _pod, _container, @@ -111,7 +110,7 @@ describe('execPodStepWithOutput', () => { it('rejects on Failure callback with no exit code in message', async () => { execSpy.mockImplementation( - ( + async ( _ns, _pod, _container, @@ -133,7 +132,7 @@ describe('execPodStepWithOutput', () => { it('retains only the last tailLines lines in the output buffer', async () => { execSpy.mockImplementation( - ( + async ( _ns, _pod, _container, @@ -166,7 +165,17 @@ describe('execPodStepWithOutput', () => { it('keeps stdout and stderr pending buffers independent (no interleaving)', async () => { execSpy.mockImplementation( - (_ns, _pod, _container, _cmd, stdout, stderr, _stdin, _tty, callback) => { + async ( + _ns, + _pod, + _container, + _cmd, + stdout, + stderr, + _stdin, + _tty, + callback + ) => { // Write partial lines to both streams without a newline delimiter. // If the pending buffers were shared, "out partial" and "err partial" // would be concatenated into one corrupted line. diff --git a/packages/k8s/tests/run-container-step-terminated-test.ts b/packages/k8s/tests/run-container-step-terminated-test.ts index 04e3038c..208162b3 100644 --- a/packages/k8s/tests/run-container-step-terminated-test.ts +++ b/packages/k8s/tests/run-container-step-terminated-test.ts @@ -44,12 +44,9 @@ function makeMinimalArgs(): RunContainerStepArgs { } describe('runContainerStep terminated error detection', () => { - let createStepPodSpy: jest.SpyInstance - let waitForPodPhasesSpy: jest.SpyInstance let getPodByNameSpy: jest.SpyInstance let getContainerTerminatedErrorsSpy: jest.SpyInstance let describePodFailureSpy: jest.SpyInstance - let deletePodSpy: jest.SpyInstance let coreErrorSpy: jest.SpyInstance beforeEach(() => { @@ -58,13 +55,11 @@ describe('runContainerStep terminated error detection', () => { process.env['GITHUB_WORKSPACE'] = '/tmp/runner/_work/repo/repo' process.env['ACTIONS_RUNNER_POD_NAME'] = 'test-runner-pod' - createStepPodSpy = jest - .spyOn(k8sModule, 'createContainerStepPod') - .mockResolvedValue({ - metadata: { name: 'test-step-pod' } - } as k8s.V1Pod) + jest.spyOn(k8sModule, 'createContainerStepPod').mockResolvedValue({ + metadata: { name: 'test-step-pod' } + } as k8s.V1Pod) - waitForPodPhasesSpy = jest + jest .spyOn(k8sModule, 'waitForPodPhases') .mockRejectedValue( new Error('Pod test-step-pod has unrecoverable errors') @@ -83,9 +78,7 @@ describe('runContainerStep terminated error detection', () => { 'Pod status: Failed\nContainer details:\n ✗ container "job" terminated: OOMKilled (exit code 137)' ) - deletePodSpy = jest - .spyOn(k8sModule, 'deletePod') - .mockResolvedValue(undefined) + jest.spyOn(k8sModule, 'deletePod').mockResolvedValue(undefined) coreErrorSpy = jest.spyOn(core, 'error') }) diff --git a/packages/k8s/tests/run-script-step-unit-test.ts b/packages/k8s/tests/run-script-step-unit-test.ts index 06b144bc..fd5c361e 100644 --- a/packages/k8s/tests/run-script-step-unit-test.ts +++ b/packages/k8s/tests/run-script-step-unit-test.ts @@ -1,4 +1,3 @@ -import * as core from '@actions/core' import { runScriptStep } from '../src/hooks' import * as k8sModule from '../src/k8s' import * as utils from '../src/k8s/utils' @@ -23,10 +22,7 @@ function makeArgs(): RunScriptStepArgs { const state = { jobPod: 'test-job-pod' } describe('runScriptStep error classification', () => { - let execPodStepSpy: jest.SpyInstance let execPodStepWithOutputSpy: jest.SpyInstance - let execCpToPodSpy: jest.SpyInstance - let execCpFromPodSpy: jest.SpyInstance beforeEach(() => { process.env['ACTIONS_RUNNER_KUBERNETES_NAMESPACE'] = 'default' @@ -39,15 +35,11 @@ describe('runScriptStep error classification', () => { runnerPath: '/tmp/test-script.sh' }) - execPodStepSpy = jest.spyOn(k8sModule, 'execPodStep').mockResolvedValue(0) + jest.spyOn(k8sModule, 'execPodStep').mockResolvedValue(0) - execCpToPodSpy = jest - .spyOn(k8sModule, 'execCpToPod') - .mockResolvedValue(undefined) + jest.spyOn(k8sModule, 'execCpToPod').mockResolvedValue(undefined) - execCpFromPodSpy = jest - .spyOn(k8sModule, 'execCpFromPod') - .mockResolvedValue(undefined) + jest.spyOn(k8sModule, 'execCpFromPod').mockResolvedValue(undefined) execPodStepWithOutputSpy = jest.spyOn(k8sModule, 'execPodStepWithOutput') }) From 52ac0e2ea4d739d9d1a7ec1d0db67a3e52090eb4 Mon Sep 17 00:00:00 2001 From: Wenl4ng <15861868579@163.com> Date: Tue, 13 Oct 2026 14:41:12 +0800 Subject: [PATCH 8/9] chore: sync package-lock.json version with package.json (0.8.0-ascend.1) Fixes CI 'Check linter' git diff --exit-code failure: npm install rewrites the lockfile version field, which must match package.json. --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 719a2da2..ee0ae37d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hooks", - "version": "0.8.0", + "version": "0.8.0-ascend.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hooks", - "version": "0.8.0", + "version": "0.8.0-ascend.1", "license": "MIT", "dependencies": { "eslint-plugin-jest": "^29.0.1" From 2377b98babe1b8fd1d4c470b2f75940670d99d52 Mon Sep 17 00:00:00 2001 From: Wenl4ng <15861868579@163.com> Date: Thu, 15 Oct 2026 09:42:58 +0800 Subject: [PATCH 9/9] test: fix wrong separator assertion in run-script-step unit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The implementation uses '-'.repeat(60) as the section separator (see run-script-step.ts / run-container-step.ts), but the test asserted the unicode box char '─'. Assert the actual separator instead. --- packages/k8s/tests/run-script-step-unit-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/k8s/tests/run-script-step-unit-test.ts b/packages/k8s/tests/run-script-step-unit-test.ts index fd5c361e..9fad839e 100644 --- a/packages/k8s/tests/run-script-step-unit-test.ts +++ b/packages/k8s/tests/run-script-step-unit-test.ts @@ -83,7 +83,7 @@ describe('runScriptStep error classification', () => { expect(caughtErr).toBeDefined() expect(caughtErr?.message).toContain('script output line') expect(caughtErr?.message).toContain('Last output:') - expect(caughtErr?.message).toContain('─') + expect(caughtErr?.message).toContain('-'.repeat(60)) }) it('throws generic error when execPodStepWithOutput rejects with non-exit error', async () => {