mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-04 04:21:35 +00:00
fix(ci): publish version-tagged preview Docker images (#8068)
This commit is contained in:
@@ -28,6 +28,7 @@ on:
|
||||
- 'scripts/release/package_versions.sh'
|
||||
- 'scripts/test_package_service_scripts.sh'
|
||||
- 'scripts/test_package_versions.sh'
|
||||
- 'scripts/test_docker_workflow.py'
|
||||
- 'scripts/security/check_performance_ab_workflow.sh'
|
||||
- 'scripts/security/check_preview_release_workflow.sh'
|
||||
- 'scripts/security/check_tier_artifact_workflow.sh'
|
||||
@@ -46,6 +47,7 @@ on:
|
||||
- 'scripts/release/package_versions.sh'
|
||||
- 'scripts/test_package_service_scripts.sh'
|
||||
- 'scripts/test_package_versions.sh'
|
||||
- 'scripts/test_docker_workflow.py'
|
||||
- 'scripts/security/check_performance_ab_workflow.sh'
|
||||
- 'scripts/security/check_preview_release_workflow.sh'
|
||||
- 'scripts/security/check_tier_artifact_workflow.sh'
|
||||
|
||||
@@ -818,9 +818,7 @@ jobs:
|
||||
|
||||
echo ""
|
||||
echo "🐳 Docker Images:"
|
||||
if [[ "$BUILD_TYPE" == "preview" ]]; then
|
||||
echo "⏭️ Preview tags do not publish Docker images"
|
||||
elif [[ "$INPUT_BUILD_DOCKER" == "false" ]]; then
|
||||
if [[ "$INPUT_BUILD_DOCKER" == "false" ]]; then
|
||||
echo "⏭️ Docker image build was skipped (binary only build)"
|
||||
elif [[ "$BUILD_STATUS" == "success" ]]; then
|
||||
echo "🔄 Docker images will be built and pushed automatically via workflow_run event"
|
||||
|
||||
@@ -47,7 +47,7 @@ on:
|
||||
default: true
|
||||
type: boolean
|
||||
version:
|
||||
description: "Version to build (latest for stable release, or specific version like v1.0.0, v1.0.0-alpha1)"
|
||||
description: "Version to build (latest, v1.0.0, v1.0.0-alpha1, or v1.0.1-preview.1)"
|
||||
required: false
|
||||
default: "latest"
|
||||
type: string
|
||||
@@ -82,8 +82,7 @@ jobs:
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.head_branch != 'main' &&
|
||||
!contains(github.event.workflow_run.head_branch, '-preview'))
|
||||
github.event.workflow_run.head_branch != 'main')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
@@ -197,7 +196,14 @@ jobs:
|
||||
version="${version#v}"
|
||||
fi
|
||||
|
||||
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
|
||||
if [[ "$version" =~ -preview\.[0-9]+$ ]]; then
|
||||
build_type="preview"
|
||||
is_prerelease=true
|
||||
echo "🔍 Building Docker image for preview: $version (version tags only)"
|
||||
elif [[ "$version" == *"-preview"* ]]; then
|
||||
echo "❌ Invalid preview tag: $version (expected suffix: -preview.<number>)" >&2
|
||||
exit 1
|
||||
elif [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
|
||||
build_type="prerelease"
|
||||
is_prerelease=true
|
||||
# Pre-GA policy: prereleases update latest until the first stable tag exists.
|
||||
@@ -249,11 +255,14 @@ jobs:
|
||||
echo "🚀 Building with latest stable release version"
|
||||
;;
|
||||
*-preview*)
|
||||
if [[ ! "$input_version" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?-preview\.[0-9]+$ ]]; then
|
||||
echo "❌ Invalid preview version: $input_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
build_type="preview"
|
||||
is_prerelease=true
|
||||
should_build=false
|
||||
should_push=false
|
||||
echo "⏭️ Preview tags do not publish Docker images"
|
||||
version="${input_version#v}"
|
||||
echo "🔍 Building with preview version: $version (version tags only)"
|
||||
;;
|
||||
# Prerelease versions (must match first, more specific)
|
||||
v*alpha*|v*beta*|v*rc*|*alpha*|*beta*|*rc*)
|
||||
@@ -611,6 +620,10 @@ jobs:
|
||||
echo ""
|
||||
|
||||
case "$BUILD_TYPE" in
|
||||
"preview")
|
||||
echo "🔍 Preview Docker image has been built with ${VERSION} tags"
|
||||
echo "⏭️ Preview images do not update latest or prerelease channels"
|
||||
;;
|
||||
"release")
|
||||
echo "🚀 Release Docker image has been built with ${VERSION} tags"
|
||||
echo "✅ This image is ready for production use"
|
||||
|
||||
@@ -203,8 +203,7 @@ IFS= read -r -d '' expected_docker_automatic_guard <<'EOF' || true
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.head_branch != 'main' &&
|
||||
!contains(github.event.workflow_run.head_branch, '-preview'))
|
||||
github.event.workflow_run.head_branch != 'main')
|
||||
EOF
|
||||
expected_docker_automatic_guard=${expected_docker_automatic_guard%$'\n'}
|
||||
require_job_if "$docker_workflow" "build-check" "$expected_docker_automatic_guard"
|
||||
@@ -216,15 +215,7 @@ require_line "$docker_workflow" ' SOURCE_REVISION="$(git rev-parse HEAD
|
||||
require_line "$docker_workflow" ' LABELS="$LABELS,org.opencontainers.image.revision=$SOURCE_REVISION"' "Docker revision label"
|
||||
require_absent "$docker_workflow" 'org.opencontainers.image.revision=${{ github.sha }}' "Docker revision must not use the workflow branch SHA"
|
||||
|
||||
docker_manual_guard=$(awk '
|
||||
$0 == " *-preview*)" { in_preview = 1 }
|
||||
in_preview { print }
|
||||
in_preview && $0 == " ;;" { exit }
|
||||
' "$docker_workflow")
|
||||
for assignment in 'build_type="preview"' 'is_prerelease=true' 'should_build=false' 'should_push=false'; do
|
||||
name="${assignment%%=*}"
|
||||
require_assignment "$docker_manual_guard" "$name" "${assignment#*=}"
|
||||
done
|
||||
python3 scripts/test_docker_workflow.py
|
||||
|
||||
IFS= read -r -d '' expected_helm_guard <<'EOF' || true
|
||||
if: |
|
||||
|
||||
Executable
+199
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise the Docker workflow's version selection and tags without publishing."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
WORKFLOW = ROOT / ".github/workflows/docker.yml"
|
||||
REGISTRIES = ("rustfs/rustfs", "ghcr.io/rustfs/rustfs", "quay.io/rustfs/rustfs")
|
||||
|
||||
|
||||
def run_body(name):
|
||||
lines = WORKFLOW.read_text().splitlines()
|
||||
start = lines.index(f" - name: {name}")
|
||||
start = lines.index(" run: |", start) + 1
|
||||
body = []
|
||||
for line in lines[start:]:
|
||||
if line.strip() and not line.startswith(" "):
|
||||
break
|
||||
body.append(line[10:])
|
||||
return "\n".join(body)
|
||||
|
||||
|
||||
class DockerWorkflowTests(unittest.TestCase):
|
||||
def run_step(self, name, context, **env):
|
||||
script = re.sub(
|
||||
r"\$\{\{\s*(.*?)\s*\}\}", lambda m: context[m[1]], run_body(name)
|
||||
)
|
||||
# Fail on unexpected Git calls so these tests cannot contact a remote.
|
||||
mock_git = """
|
||||
git() {
|
||||
if [[ "$1" == rev-parse ]]; then
|
||||
echo 0123456789abcdef
|
||||
elif [[ "$1 $2 $3" == "ls-remote --tags --refs" ]]; then
|
||||
if [[ "$STABLE_EXISTS" == true ]]; then
|
||||
echo "0123456789abcdef refs/tags/v1.0.0"
|
||||
fi
|
||||
elif [[ "$1 $2" == "ls-remote --exit-code" ]]; then
|
||||
[[ "$4" == "$EXISTING_TAG" ]]
|
||||
else
|
||||
return 99
|
||||
fi
|
||||
}
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory) / "output"
|
||||
result = subprocess.run(
|
||||
["bash", "-e", "-o", "pipefail", "-c", mock_git + script],
|
||||
env={**os.environ, "GITHUB_OUTPUT": str(output), **env},
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
values = (
|
||||
dict(line.split("=", 1) for line in output.read_text().splitlines())
|
||||
if output.exists()
|
||||
else {}
|
||||
)
|
||||
return result, values
|
||||
|
||||
def classify(
|
||||
self, version, event="workflow_run", push="true", stable="true", tag=None, **env
|
||||
):
|
||||
return self.run_step(
|
||||
"Check build conditions",
|
||||
{"github.event_name": event},
|
||||
GITHUB_SHA="workflow-sha",
|
||||
HEAD_SHA="release-sha",
|
||||
HEAD_BRANCH=version,
|
||||
INPUT_VERSION=version,
|
||||
INPUT_PUSH_IMAGES=push,
|
||||
INPUT_FORCE_REBUILD="false",
|
||||
STABLE_EXISTS=stable,
|
||||
EXISTING_TAG=tag or f"refs/tags/{version}",
|
||||
**{"CONCLUSION": "success", "TRIGGERING_EVENT": "push", **env},
|
||||
)
|
||||
|
||||
def assert_tags(self, values, suffix="", channels=()):
|
||||
context = {
|
||||
f"needs.build-check.outputs.{key}": value for key, value in values.items()
|
||||
}
|
||||
context.update(
|
||||
{
|
||||
"matrix.suffix": suffix,
|
||||
"env.REGISTRY_DOCKERHUB": REGISTRIES[0],
|
||||
"env.REGISTRY_GHCR": REGISTRIES[1],
|
||||
"env.REGISTRY_QUAY": REGISTRIES[2],
|
||||
"github.server_url": "https://github.com",
|
||||
"github.repository": "rustfs/rustfs",
|
||||
}
|
||||
)
|
||||
result, metadata = self.run_step("Extract metadata and generate tags", context)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
expected = {
|
||||
f"{registry}:{tag}{suffix}"
|
||||
for registry in REGISTRIES
|
||||
for tag in (values["version"], *channels)
|
||||
}
|
||||
self.assertEqual(set(metadata["tags"].split(",")), expected)
|
||||
|
||||
def test_preview_version_tags_only(self):
|
||||
versions = (
|
||||
"1.0.1-preview.8",
|
||||
"v1.0.1-preview.8",
|
||||
"1.0.0-alpha.1-preview.2",
|
||||
"1.0.0-beta.1-preview.2",
|
||||
"v1.0.0-rc.1-preview.2",
|
||||
)
|
||||
for event in ("workflow_run", "workflow_dispatch"):
|
||||
for version in versions:
|
||||
for stable in ("true", "false"):
|
||||
with self.subTest(event=event, version=version, stable=stable):
|
||||
result, values = self.classify(version, event, stable=stable)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
for key, expected in {
|
||||
"should_build": "true",
|
||||
"should_push": "true",
|
||||
"build_type": "preview",
|
||||
"is_prerelease": "true",
|
||||
"create_latest": "false",
|
||||
"version": version.removeprefix("v"),
|
||||
}.items():
|
||||
self.assertEqual(values[key], expected, key)
|
||||
expected_ref = (
|
||||
"release-sha"
|
||||
if event == "workflow_run"
|
||||
else f"refs/tags/{version}"
|
||||
)
|
||||
self.assertEqual(values["source_ref"], expected_ref)
|
||||
for suffix in ("", "-glibc"):
|
||||
self.assert_tags(values, suffix)
|
||||
|
||||
def test_manual_preview_dry_run_and_tag_fallback(self):
|
||||
result, values = self.classify(
|
||||
"1.0.1-preview.8",
|
||||
"workflow_dispatch",
|
||||
push="false",
|
||||
tag="refs/tags/v1.0.1-preview.8",
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(values["should_build"], "true")
|
||||
self.assertEqual(values["should_push"], "false")
|
||||
self.assertEqual(values["source_ref"], "refs/tags/v1.0.1-preview.8")
|
||||
|
||||
def test_full_tag_ref(self):
|
||||
result, values = self.classify("refs/tags/v1.0.1-preview.8")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(values["build_type"], "preview")
|
||||
self.assertEqual(values["version"], "1.0.1-preview.8")
|
||||
self.assert_tags(values)
|
||||
|
||||
def test_invalid_preview_and_missing_tag_fail(self):
|
||||
for event in ("workflow_run", "workflow_dispatch"):
|
||||
for version in (
|
||||
"1.0.1-preview",
|
||||
"1.0.1-preview.x",
|
||||
"1.0.1-preview.8-extra",
|
||||
):
|
||||
with self.subTest(event=event, version=version):
|
||||
result, _ = self.classify(version, event)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
result, _ = self.classify(
|
||||
"1.0.1-preview.8", "workflow_dispatch", tag="refs/tags/missing"
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
|
||||
def test_existing_release_channels(self):
|
||||
for event in ("workflow_run", "workflow_dispatch"):
|
||||
for stable in ("true", "false"):
|
||||
for channel in (None, "alpha", "beta", "rc"):
|
||||
version = f"1.0.1-{channel}.1" if channel else "1.0.1"
|
||||
with self.subTest(event=event, stable=stable, channel=channel):
|
||||
result, values = self.classify(version, event, stable=stable)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
latest = channel is None or stable == "false"
|
||||
self.assertEqual(values["create_latest"], str(latest).lower())
|
||||
channels = ([channel] if channel else []) + (
|
||||
["latest"] if latest else []
|
||||
)
|
||||
self.assert_tags(values, channels=channels)
|
||||
|
||||
def test_non_release_builds_are_skipped(self):
|
||||
for version, env in (
|
||||
("main", {}),
|
||||
("feature/test", {}),
|
||||
("1.0.1-preview.8", {"CONCLUSION": "failure"}),
|
||||
("1.0.1-preview.8", {"TRIGGERING_EVENT": "workflow_dispatch"}),
|
||||
):
|
||||
result, values = self.classify(version, **env)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(values["should_build"], "false")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user