diff --git a/.github/workflows/rustfs-kms-test.yml b/.github/workflows/rustfs-kms-test.yml
index 07f80d520..a8738216d 100644
--- a/.github/workflows/rustfs-kms-test.yml
+++ b/.github/workflows/rustfs-kms-test.yml
@@ -38,6 +38,7 @@ env:
jobs:
kms-test:
runs-on: smoke-testing
+ continue-on-error: true
timeout-minutes: 420
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
steps:
@@ -86,6 +87,7 @@ jobs:
- name: Run KMS suite
id: test
+ continue-on-error: true
env:
LOG_FILE: /tmp/rustfs-kms.log
run: |
@@ -119,12 +121,65 @@ jobs:
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
+ CASE_TABLE="/tmp/rustfs-kms-cases.md"
+ python3 - "${LOG_FILE}" "${CASE_TABLE}" <<'PY'
+ import re
+ import sys
+
+ log_file, out_file = sys.argv[1], sys.argv[2]
+ ansi = re.compile(r'\x1b\[[0-9;]*m')
+ start_re = re.compile(r'^---\s+([A-Z]+-[0-9]+)\s+(.+?)\s+---$')
+ done_re = re.compile(r'^\[(PASS|FAIL|UNSUPPORTED)\]\s+([A-Z]+-[0-9]+)\b')
+
+ rows = []
+ index = {}
+ try:
+ with open(log_file, 'r', encoding='utf-8', errors='replace') as fh:
+ for raw in fh:
+ line = ansi.sub('', raw).strip()
+ m = start_re.match(line)
+ if m:
+ case_id, name = m.group(1), m.group(2)
+ if case_id not in index:
+ index[case_id] = len(rows)
+ rows.append([case_id, name, 'RUNNING'])
+ continue
+ m = done_re.match(line)
+ if m:
+ status, case_id = m.group(1), m.group(2)
+ if case_id in index:
+ rows[index[case_id]][2] = status
+ else:
+ rows.append([case_id, case_id, status])
+ index[case_id] = len(rows) - 1
+ except FileNotFoundError:
+ rows = []
+
+ counts = {'PASS': 0, 'FAIL': 0, 'UNSUPPORTED': 0, 'RUNNING': 0}
+ for _, _, status in rows:
+ counts[status] = counts.get(status, 0) + 1
+
+ with open(out_file, 'w', encoding='utf-8') as out:
+ out.write('## Case Summary\n\n')
+ out.write(f"- Total: {len(rows)}\\n")
+ out.write(f"- PASS: {counts.get('PASS', 0)}\\n")
+ out.write(f"- FAIL: {counts.get('FAIL', 0)}\\n")
+ out.write(f"- UNSUPPORTED: {counts.get('UNSUPPORTED', 0)}\\n")
+ out.write('\\n')
+ out.write('| Case | Name | Status |\\n')
+ out.write('| --- | --- | --- |\\n')
+ for case_id, name, status in rows:
+ out.write(f'| {case_id} | {name} | {status} |\\n')
+ PY
{
echo "# RustFS KMS test report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Trigger: ${{ github.event_name }}"
echo "- Package: ${PACKAGE_SOURCE}"
+ echo "- Test Step Outcome: ${{ steps.test.outcome }}"
+ echo ""
+ cat "${CASE_TABLE}" || true
echo ""
echo "## Log tail"
echo '```text'
@@ -133,6 +188,129 @@ jobs:
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
+ - name: Upload functional report to dashboard
+ if: always()
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
+ REPORT_FILE: /tmp/rustfs-kms-report.md
+ SUITE: kms
+ run: |
+ set -euo pipefail
+ if [ -z "${GH_TOKEN:-}" ]; then
+ echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
+ exit 0
+ fi
+ DATE="$(date -u +%Y-%m-%d)"
+ REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
+ CONTENT="$(python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}")"
+ SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
+ if [ -n "${SHA}" ]; then
+ jq -n --arg msg "report(${SUITE}): ${DATE}" --arg content "${CONTENT}" --arg sha "${SHA}" \
+ '{message:$msg, content:$content, sha:$sha}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
+ else
+ jq -n --arg msg "report(${SUITE}): ${DATE}" --arg content "${CONTENT}" \
+ '{message:$msg, content:$content}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
+ fi
+
+ cat > /tmp/rustfs-functional-index.html <<'EOF'
+
+
+
+
+
+ RustFS Functional Test Reports
+
+
+
+
+
+
RustFS Functional Test Reports
+
S3, KMS, Tier report tabs. Each tab lists reports by date.
+
+
+
+
+
+
+
+ EOF
+
+ INDEX_PATH="functional/index.html"
+ INDEX_CONTENT="$(python3 -c 'import base64;print(base64.b64encode(open("/tmp/rustfs-functional-index.html","rb").read()).decode())')"
+ INDEX_SHA="$(gh api "repos/rustfs/dashboard/contents/${INDEX_PATH}" -q '.sha' 2>/dev/null || true)"
+ if [ -n "${INDEX_SHA}" ]; then
+ jq -n --arg msg "functional ui update" --arg content "${INDEX_CONTENT}" --arg sha "${INDEX_SHA}" \
+ '{message:$msg, content:$content, sha:$sha}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${INDEX_PATH}" --input - >/dev/null
+ else
+ jq -n --arg msg "functional ui init" --arg content "${INDEX_CONTENT}" \
+ '{message:$msg, content:$content}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${INDEX_PATH}" --input - >/dev/null
+ fi
+
- name: Upload report and logs
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
diff --git a/.github/workflows/rustfs-s3-compat-test.yml b/.github/workflows/rustfs-s3-compat-test.yml
index 2efe2986b..fffd0621f 100644
--- a/.github/workflows/rustfs-s3-compat-test.yml
+++ b/.github/workflows/rustfs-s3-compat-test.yml
@@ -33,10 +33,12 @@ env:
RUSTFS_NODES: ${{ secrets.RUSTFS_NODES || vars.RUSTFS_NODES }}
RUSTFS_SSH_USER: ${{ secrets.RUSTFS_SSH_USER || vars.RUSTFS_SSH_USER }}
RUSTFS_NIGHTLY_PACKAGE_URL: ${{ vars.RUSTFS_NIGHTLY_PACKAGE_URL || 'https://dl.rustfs.com/artifacts/rustfs/packages/nightly/rustfs-nightly-latest.deb' }}
+ PF_TESTING_GH_TOKEN: ${{ secrets.PF_TESTING_GH_TOKEN }}
jobs:
s3-compat-test:
runs-on: smoke-testing
+ continue-on-error: true
timeout-minutes: 360
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
steps:
@@ -76,6 +78,7 @@ jobs:
- name: Run S3 compatibility suite
id: test
+ continue-on-error: true
env:
LOG_FILE: /tmp/rustfs-s3-compat.log
run: |
@@ -109,12 +112,68 @@ jobs:
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
+ CASE_TABLE="/tmp/rustfs-s3-compat-cases.md"
+ python3 - "${LOG_FILE}" "${CASE_TABLE}" <<'PY'
+ import re
+ import sys
+
+ log_file, out_file = sys.argv[1], sys.argv[2]
+ ansi = re.compile(r'\x1b\[[0-9;]*m')
+ start_re = re.compile(r'^---\s+([A-Z]+-[0-9]+)\s+(.+?)\s+---$')
+ done_re = re.compile(r'^\[(PASS|FAIL|UNSUPPORTED)\]\s+([A-Z]+-[0-9]+)\b')
+
+ rows = []
+ index = {}
+ current = None
+ try:
+ with open(log_file, 'r', encoding='utf-8', errors='replace') as fh:
+ for raw in fh:
+ line = ansi.sub('', raw).strip()
+ m = start_re.match(line)
+ if m:
+ case_id, name = m.group(1), m.group(2)
+ current = case_id
+ if case_id not in index:
+ index[case_id] = len(rows)
+ rows.append([case_id, name, 'RUNNING'])
+ continue
+ m = done_re.match(line)
+ if m:
+ status, case_id = m.group(1), m.group(2)
+ if case_id in index:
+ rows[index[case_id]][2] = status
+ else:
+ rows.append([case_id, case_id, status])
+ index[case_id] = len(rows) - 1
+ current = None
+ except FileNotFoundError:
+ rows = []
+
+ counts = {'PASS': 0, 'FAIL': 0, 'UNSUPPORTED': 0, 'RUNNING': 0}
+ for _, _, status in rows:
+ counts[status] = counts.get(status, 0) + 1
+
+ with open(out_file, 'w', encoding='utf-8') as out:
+ out.write('## Case Summary\n\n')
+ out.write(f"- Total: {len(rows)}\\n")
+ out.write(f"- PASS: {counts.get('PASS', 0)}\\n")
+ out.write(f"- FAIL: {counts.get('FAIL', 0)}\\n")
+ out.write(f"- UNSUPPORTED: {counts.get('UNSUPPORTED', 0)}\\n")
+ out.write('\\n')
+ out.write('| Case | Name | Status |\\n')
+ out.write('| --- | --- | --- |\\n')
+ for case_id, name, status in rows:
+ out.write(f'| {case_id} | {name} | {status} |\\n')
+ PY
{
echo "# RustFS S3 compatibility test report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Trigger: ${{ github.event_name }}"
echo "- Package: ${PACKAGE_SOURCE}"
+ echo "- Test Step Outcome: ${{ steps.test.outcome }}"
+ echo ""
+ cat "${CASE_TABLE}" || true
echo ""
echo "## Log tail"
echo '```text'
@@ -123,6 +182,129 @@ jobs:
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
+ - name: Upload functional report to dashboard
+ if: always()
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
+ REPORT_FILE: /tmp/rustfs-s3-compat-report.md
+ SUITE: s3
+ run: |
+ set -euo pipefail
+ if [ -z "${GH_TOKEN:-}" ]; then
+ echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
+ exit 0
+ fi
+ DATE="$(date -u +%Y-%m-%d)"
+ REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
+ CONTENT="$(python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}")"
+ SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
+ if [ -n "${SHA}" ]; then
+ jq -n --arg msg "report(${SUITE}): ${DATE}" --arg content "${CONTENT}" --arg sha "${SHA}" \
+ '{message:$msg, content:$content, sha:$sha}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
+ else
+ jq -n --arg msg "report(${SUITE}): ${DATE}" --arg content "${CONTENT}" \
+ '{message:$msg, content:$content}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
+ fi
+
+ cat > /tmp/rustfs-functional-index.html <<'EOF'
+
+
+
+
+
+ RustFS Functional Test Reports
+
+
+
+
+
+
RustFS Functional Test Reports
+
S3, KMS, Tier report tabs. Each tab lists reports by date.
+
+
+
+
+
+
+
+ EOF
+
+ INDEX_PATH="functional/index.html"
+ INDEX_CONTENT="$(python3 -c 'import base64;print(base64.b64encode(open("/tmp/rustfs-functional-index.html","rb").read()).decode())')"
+ INDEX_SHA="$(gh api "repos/rustfs/dashboard/contents/${INDEX_PATH}" -q '.sha' 2>/dev/null || true)"
+ if [ -n "${INDEX_SHA}" ]; then
+ jq -n --arg msg "functional ui update" --arg content "${INDEX_CONTENT}" --arg sha "${INDEX_SHA}" \
+ '{message:$msg, content:$content, sha:$sha}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${INDEX_PATH}" --input - >/dev/null
+ else
+ jq -n --arg msg "functional ui init" --arg content "${INDEX_CONTENT}" \
+ '{message:$msg, content:$content}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${INDEX_PATH}" --input - >/dev/null
+ fi
+
- name: Upload report and logs
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
diff --git a/.github/workflows/rustfs-tier-test.yml b/.github/workflows/rustfs-tier-test.yml
index 2fda50b70..acad9387f 100644
--- a/.github/workflows/rustfs-tier-test.yml
+++ b/.github/workflows/rustfs-tier-test.yml
@@ -38,6 +38,7 @@ env:
jobs:
tier-test:
runs-on: smoke-testing
+ continue-on-error: true
timeout-minutes: 420
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
steps:
@@ -107,6 +108,7 @@ jobs:
- name: Run tier suite
id: test
+ continue-on-error: true
env:
LOG_FILE: /tmp/rustfs-tier.log
run: |
@@ -140,12 +142,65 @@ jobs:
else
PACKAGE_SOURCE="${RUSTFS_NIGHTLY_PACKAGE_URL}"
fi
+ CASE_TABLE="/tmp/rustfs-tier-cases.md"
+ python3 - "${LOG_FILE}" "${CASE_TABLE}" <<'PY'
+ import re
+ import sys
+
+ log_file, out_file = sys.argv[1], sys.argv[2]
+ ansi = re.compile(r'\x1b\[[0-9;]*m')
+ start_re = re.compile(r'^---\s+([A-Z]+-[0-9]+)\s+(.+?)\s+---$')
+ done_re = re.compile(r'^\[(PASS|FAIL|UNSUPPORTED)\]\s+([A-Z]+-[0-9]+)\b')
+
+ rows = []
+ index = {}
+ try:
+ with open(log_file, 'r', encoding='utf-8', errors='replace') as fh:
+ for raw in fh:
+ line = ansi.sub('', raw).strip()
+ m = start_re.match(line)
+ if m:
+ case_id, name = m.group(1), m.group(2)
+ if case_id not in index:
+ index[case_id] = len(rows)
+ rows.append([case_id, name, 'RUNNING'])
+ continue
+ m = done_re.match(line)
+ if m:
+ status, case_id = m.group(1), m.group(2)
+ if case_id in index:
+ rows[index[case_id]][2] = status
+ else:
+ rows.append([case_id, case_id, status])
+ index[case_id] = len(rows) - 1
+ except FileNotFoundError:
+ rows = []
+
+ counts = {'PASS': 0, 'FAIL': 0, 'UNSUPPORTED': 0, 'RUNNING': 0}
+ for _, _, status in rows:
+ counts[status] = counts.get(status, 0) + 1
+
+ with open(out_file, 'w', encoding='utf-8') as out:
+ out.write('## Case Summary\n\n')
+ out.write(f"- Total: {len(rows)}\\n")
+ out.write(f"- PASS: {counts.get('PASS', 0)}\\n")
+ out.write(f"- FAIL: {counts.get('FAIL', 0)}\\n")
+ out.write(f"- UNSUPPORTED: {counts.get('UNSUPPORTED', 0)}\\n")
+ out.write('\\n')
+ out.write('| Case | Name | Status |\\n')
+ out.write('| --- | --- | --- |\\n')
+ for case_id, name, status in rows:
+ out.write(f'| {case_id} | {name} | {status} |\\n')
+ PY
{
echo "# RustFS tier test report"
echo ""
echo "- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "- Trigger: ${{ github.event_name }}"
echo "- Package: ${PACKAGE_SOURCE}"
+ echo "- Test Step Outcome: ${{ steps.test.outcome }}"
+ echo ""
+ cat "${CASE_TABLE}" || true
echo ""
echo "## Log tail"
echo '```text'
@@ -154,6 +209,129 @@ jobs:
} | tee "${REPORT_FILE}"
cat "${REPORT_FILE}" >> "${GITHUB_STEP_SUMMARY}"
+ - name: Upload functional report to dashboard
+ if: always()
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ env.PF_TESTING_GH_TOKEN }}
+ REPORT_FILE: /tmp/rustfs-tier-report.md
+ SUITE: tier
+ run: |
+ set -euo pipefail
+ if [ -z "${GH_TOKEN:-}" ]; then
+ echo "PF_TESTING_GH_TOKEN is not configured; skipping dashboard upload"
+ exit 0
+ fi
+ DATE="$(date -u +%Y-%m-%d)"
+ REPORT_PATH="functional-reports/${SUITE}/${DATE}.md"
+ CONTENT="$(python3 -c 'import base64,sys;print(base64.b64encode(open(sys.argv[1],"rb").read()).decode())' "${REPORT_FILE}")"
+ SHA="$(gh api "repos/rustfs/dashboard/contents/${REPORT_PATH}" -q '.sha' 2>/dev/null || true)"
+ if [ -n "${SHA}" ]; then
+ jq -n --arg msg "report(${SUITE}): ${DATE}" --arg content "${CONTENT}" --arg sha "${SHA}" \
+ '{message:$msg, content:$content, sha:$sha}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
+ else
+ jq -n --arg msg "report(${SUITE}): ${DATE}" --arg content "${CONTENT}" \
+ '{message:$msg, content:$content}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${REPORT_PATH}" --input - >/dev/null
+ fi
+
+ cat > /tmp/rustfs-functional-index.html <<'EOF'
+
+
+
+
+
+ RustFS Functional Test Reports
+
+
+
+
+
+
RustFS Functional Test Reports
+
S3, KMS, Tier report tabs. Each tab lists reports by date.
+
+
+
+
+
+
+
+ EOF
+
+ INDEX_PATH="functional/index.html"
+ INDEX_CONTENT="$(python3 -c 'import base64;print(base64.b64encode(open("/tmp/rustfs-functional-index.html","rb").read()).decode())')"
+ INDEX_SHA="$(gh api "repos/rustfs/dashboard/contents/${INDEX_PATH}" -q '.sha' 2>/dev/null || true)"
+ if [ -n "${INDEX_SHA}" ]; then
+ jq -n --arg msg "functional ui update" --arg content "${INDEX_CONTENT}" --arg sha "${INDEX_SHA}" \
+ '{message:$msg, content:$content, sha:$sha}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${INDEX_PATH}" --input - >/dev/null
+ else
+ jq -n --arg msg "functional ui init" --arg content "${INDEX_CONTENT}" \
+ '{message:$msg, content:$content}' \
+ | gh api --method PUT "repos/rustfs/dashboard/contents/${INDEX_PATH}" --input - >/dev/null
+ fi
+
- name: Upload report and logs
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6