feat(web-release): enhance release metadata handling and testing

- Updated `release_metadata` function to support manual testing from the main branch without publishing.
- Added tests to verify behavior for manual main runs, tag releases, and restrictions on other branches.
- Modified workflow to conditionally publish images based on the event type and added logging for manual test results.
- Enhanced documentation to clarify manual testing procedures and workflow behavior.
This commit is contained in:
Kang committed 2026-10-07 21:28:59 +08:00
1 parent 29be430b6a
commit 446e1a768c
5 files changed
+57 -11

No files matched your search

+20
View File
@@ -24,6 +24,7 @@ class VersionTests(unittest.TestCase):
self.assertEqual(metadata["version"], version) self.assertEqual(metadata["version"], version)
self.assertEqual(metadata["image_tag"], version) self.assertEqual(metadata["image_tag"], version)
self.assertEqual(metadata["stable"], stable) self.assertEqual(metadata["stable"], stable)
self.assertEqual(metadata["publish"], "true")
self.assertEqual(metadata["image"], "ghcr.io/nyakang/nyaterm-web") self.assertEqual(metadata["image"], "ghcr.io/nyakang/nyaterm-web")
def test_build_metadata_is_preserved_in_version_and_encoded_in_tag(self): def test_build_metadata_is_preserved_in_version_and_encoded_in_tag(self):
@@ -38,6 +39,25 @@ class VersionTests(unittest.TestCase):
with self.subTest(ref=ref), self.assertRaises(ValueError): with self.subTest(ref=ref), self.assertRaises(ValueError):
release.release_metadata(ref, "1.2.12", "nyakang/nyaterm") release.release_metadata(ref, "1.2.12", "nyakang/nyaterm")
def test_manual_main_runs_without_publishing(self):
metadata = release.release_metadata(
"refs/heads/main", "1.2.12", "nyakang/nyaterm", "workflow_dispatch"
)
self.assertEqual(metadata["version"], "1.2.12")
self.assertEqual(metadata["publish"], "false")
def test_manual_tag_still_publishes(self):
metadata = release.release_metadata(
"refs/tags/v1.2.12", "1.2.12", "nyakang/nyaterm", "workflow_dispatch"
)
self.assertEqual(metadata["publish"], "true")
def test_other_branches_cannot_be_manually_released(self):
with self.assertRaises(ValueError):
release.release_metadata(
"refs/heads/feature", "1.2.12", "nyakang/nyaterm", "workflow_dispatch"
)
def test_invalid_semver_fails(self): def test_invalid_semver_fails(self):
for version in [ for version in [
"1.2", "1.2",
+14 -5
View File
@@ -33,10 +33,13 @@ def version_parts(version):
return tuple(int(match[i]) for i in (1, 2, 3)), prerelease return tuple(int(match[i]) for i in (1, 2, 3)), prerelease
def release_metadata(ref, package_version, repository): def release_metadata(ref, package_version, repository, event_name="push"):
if not ref.startswith("refs/tags/v"): test_only = ref == "refs/heads/main" and event_name == "workflow_dispatch"
raise ValueError("Dispatch against a version tag (refs/tags/v<SemVer>).") if not test_only and not ref.startswith("refs/tags/v"):
version = ref.removeprefix("refs/tags/v") raise ValueError(
"Select main for a manual test, or a version tag for a release."
)
version = package_version if test_only else ref.removeprefix("refs/tags/v")
_, prerelease = version_parts(version) _, prerelease = version_parts(version)
if version != package_version: if version != package_version:
raise ValueError( raise ValueError(
@@ -51,6 +54,7 @@ def release_metadata(ref, package_version, repository):
"version": version, "version": version,
"image_tag": image_tag, "image_tag": image_tag,
"stable": str(prerelease is None).lower(), "stable": str(prerelease is None).lower(),
"publish": str(not test_only).lower(),
"image": f"ghcr.io/{owner}/nyaterm-web", "image": f"ghcr.io/{owner}/nyaterm-web",
} }
@@ -199,11 +203,16 @@ def main():
os.environ["GITHUB_REF"], os.environ["GITHUB_REF"],
json.loads(Path("package.json").read_text())["version"], json.loads(Path("package.json").read_text())["version"],
os.environ["GITHUB_REPOSITORY"], os.environ["GITHUB_REPOSITORY"],
os.environ["GITHUB_EVENT_NAME"],
) )
with open(os.environ["GITHUB_OUTPUT"], "a") as output: with open(os.environ["GITHUB_OUTPUT"], "a") as output:
for key, value in metadata.items(): for key, value in metadata.items():
output.write(f"{key}={value}\n") output.write(f"{key}={value}\n")
print(f"Validated Web release {metadata['version']}") mode = "release" if metadata["publish"] == "true" else "test (no publishing)"
message = f"Validated Web {mode}: {metadata['version']}"
print(message)
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as summary:
summary.write(message + "\n")
elif args.command == "check-manifest": elif args.command == "check-manifest":
digest = verify_manifest( digest = verify_manifest(
Path("manifest.json").read_bytes(), Path("manifest.json").read_bytes(),
+19 -6
View File
@@ -22,23 +22,24 @@ jobs:
image_tag: ${{ steps.metadata.outputs.image_tag }} image_tag: ${{ steps.metadata.outputs.image_tag }}
image: ${{ steps.metadata.outputs.image }} image: ${{ steps.metadata.outputs.image }}
stable: ${{ steps.metadata.outputs.stable }} stable: ${{ steps.metadata.outputs.stable }}
publish: ${{ steps.metadata.outputs.publish }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Test release policy - name: Test release policy
run: python3 -m unittest discover -s .github/scripts -p 'test_web_release.py' run: python3 -m unittest discover -s .github/scripts -p 'test_web_release.py'
- name: Validate version tag - name: Resolve release or manual test mode
id: metadata id: metadata
run: python3 .github/scripts/web-release.py metadata run: python3 .github/scripts/web-release.py metadata
- name: Verify tag commit belongs to main - name: Verify selected commit belongs to main
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
git fetch origin main:refs/remotes/origin/main git fetch origin main:refs/remotes/origin/main
tag_commit=$(git rev-parse "${GITHUB_REF}^{commit}") ref_commit=$(git rev-parse "${GITHUB_REF}^{commit}")
if [ "$tag_commit" != "$(git rev-parse HEAD)" ] || ! git merge-base --is-ancestor "$tag_commit" origin/main; then if [ "$ref_commit" != "$(git rev-parse HEAD)" ] || ! git merge-base --is-ancestor "$ref_commit" origin/main; then
echo "::error::The checked-out tag commit must belong to origin/main." echo "::error::The checked-out commit must belong to origin/main."
exit 1 exit 1
fi fi
@@ -111,22 +112,32 @@ jobs:
env: env:
NYATERM_SMOKE_IMAGE: nyaterm-web:smoke NYATERM_SMOKE_IMAGE: nyaterm-web:smoke
run: node .github/scripts/web-release-smoke.mjs run: node .github/scripts/web-release-smoke.mjs
- name: Record manual test result
if: needs.prepare.outputs.publish != 'true'
env:
ARCH: ${{ matrix.arch }}
run: |
echo "Manual main test passed: linux/$ARCH, version $VERSION. No image was published." >> "$GITHUB_STEP_SUMMARY"
- uses: actions/setup-go@v6 - uses: actions/setup-go@v6
if: needs.prepare.outputs.publish == 'true'
with: with:
go-version: stable go-version: stable
cache: false cache: false
- name: Install crane - name: Install crane
if: needs.prepare.outputs.publish == 'true'
env: env:
GOBIN: ${{ runner.temp }}/web-tools GOBIN: ${{ runner.temp }}/web-tools
run: | run: |
go install github.com/google/go-containerregistry/cmd/crane@v0.20.3 go install github.com/google/go-containerregistry/cmd/crane@v0.20.3
echo "$GOBIN" >> "$GITHUB_PATH" echo "$GOBIN" >> "$GITHUB_PATH"
- uses: docker/login-action@v3 - uses: docker/login-action@v3
if: needs.prepare.outputs.publish == 'true'
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Push tested image by digest - name: Push tested image by digest
if: needs.prepare.outputs.publish == 'true'
env: env:
ARCH: ${{ matrix.arch }} ARCH: ${{ matrix.arch }}
shell: bash shell: bash
@@ -144,6 +155,7 @@ jobs:
echo "Image: $IMAGE@$digest" echo "Image: $IMAGE@$digest"
} >> "$GITHUB_STEP_SUMMARY" } >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: needs.prepare.outputs.publish == 'true'
with: with:
name: web-release-digest-${{ matrix.arch }} name: web-release-digest-${{ matrix.arch }}
path: digests/ path: digests/
@@ -159,6 +171,7 @@ jobs:
publish: publish:
name: Publish multi-architecture version name: Publish multi-architecture version
needs: [prepare, build] needs: [prepare, build]
if: needs.prepare.outputs.publish == 'true'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
timeout-minutes: 10 timeout-minutes: 10
permissions: permissions:
@@ -209,7 +222,7 @@ jobs:
latest: latest:
name: Promote newer stable version to latest name: Promote newer stable version to latest
needs: [prepare, publish] needs: [prepare, publish]
if: needs.prepare.outputs.stable == 'true' if: needs.prepare.outputs.publish == 'true' && needs.prepare.outputs.stable == 'true'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
timeout-minutes: 10 timeout-minutes: 10
permissions: permissions:
+2
View File
@@ -47,6 +47,8 @@ docker compose -f deploy/web/docker-compose.yml down
[Build and Release Web](../../.github/workflows/build-web-release.yml) 在推送 `v<SemVer>` 标签时自动构建,也可在 Actions 中选择版本标签手动补发。标签版本必须与 `package.json` 一致,且提交属于 `main`。镜像发版与桌面发版独立,不等待 GitHub Release 公开。 [Build and Release Web](../../.github/workflows/build-web-release.yml) 在推送 `v<SemVer>` 标签时自动构建,也可在 Actions 中选择版本标签手动补发。标签版本必须与 `package.json` 一致,且提交属于 `main`。镜像发版与桌面发版独立,不等待 GitHub Release 公开。
测试 workflow 时,在 **Actions → Build and Release Web → Run workflow** 中选择 `main`。此模式运行完整部署 E2E、amd64 / arm64 构建及容器冒烟测试,不登录 GHCR、不发布镜像,也不更新 `latest`;结果显示在 Actions 摘要中。手动选择版本标签仍执行正常发版。Workflow 需先合入默认分支,才会出现在手动运行列表中。
完整版本标签不带 `v`,如 `1.2.12`、`1.3.0-beta.1`。SemVer 构建元数据中的 `+` 在镜像标签中编码为 `_`,OCI 版本标签仍保留原始版本。预发布、旧版本及同版本补发均不会更新 `latest`,也不发布 major/minor 浮动标签。版本和架构 digest 可在 Actions 摘要中查看。 完整版本标签不带 `v`,如 `1.2.12`、`1.3.0-beta.1`。SemVer 构建元数据中的 `+` 在镜像标签中编码为 `_`,OCI 版本标签仍保留原始版本。预发布、旧版本及同版本补发均不会更新 `latest`,也不发布 major/minor 浮动标签。版本和架构 digest 可在 Actions 摘要中查看。
首次发布后,维护者需在 GitHub 的 `nyaterm-web` package 设置中确认可见性为 **Public**,以支持匿名拉取。Workflow 使用仓库的 `GITHUB_TOKEN` 发布,无需额外注册表密码;仓库及组织策略须允许 GitHub Actions 写入该 package。 首次发布后,维护者需在 GitHub 的 `nyaterm-web` package 设置中确认可见性为 **Public**,以支持匿名拉取。Workflow 使用仓库的 `GITHUB_TOKEN` 发布,无需额外注册表密码;仓库及组织策略须允许 GitHub Actions 写入该 package。
+2
View File
@@ -62,6 +62,8 @@ docker compose -f deploy/web/docker-compose.image.yml up -d
`latest` 仅由更高稳定版本更新;预发布及旧版本补发不会使它回退。首次发布后维护者需确认 GHCR package 为 Public,才能匿名拉取。发版触发条件、版本标签规则和补发入口见 [镜像发版说明](../deploy/web/README.md#镜像发版)。 `latest` 仅由更高稳定版本更新;预发布及旧版本补发不会使它回退。首次发布后维护者需确认 GHCR package 为 Public,才能匿名拉取。发版触发条件、版本标签规则和补发入口见 [镜像发版说明](../deploy/web/README.md#镜像发版)。
如需先测试发版 workflow,可在 Actions 中手动选择 `main`,运行完整部署验收及双架构构建;此模式不发布镜像或修改 `latest`。选择版本标签则正常发版。
官方镜像构建路径固定为 `/`,运行时不能仅修改 `NYATERM_WEB_BASE_PATH` 来改为子路径;子路径部署需使用原源码构建方式。镜像 Compose 沿用原来的 `8080:8080`,使用本机反向代理时应改为 `127.0.0.1:8080:8080`。 官方镜像构建路径固定为 `/`,运行时不能仅修改 `NYATERM_WEB_BASE_PATH` 来改为子路径;子路径部署需使用原源码构建方式。镜像 Compose 沿用原来的 `8080:8080`,使用本机反向代理时应改为 `127.0.0.1:8080:8080`。
### 从源码构建 ### 从源码构建