snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

358
.github/scripts/migration_automation.py vendored Normal file
View File

@@ -0,0 +1,358 @@
#!/usr/bin/env python3
"""
migration_automation.py
Triggered by GitHub Actions when a new .up.sql file lands on master.
Each Mattermost migration ships as a pair of files: NNNNNN_slug.up.sql
(applied on upgrade) and NNNNNN_slug.down.sql (applied on rollback). The
workflow filters on .up.sql files only because:
- The .up.sql is the canonical identifier for a new migration.
- Both files are committed together, so detecting the up file is enough
to locate the pair.
- We never want to trigger a separate review run for a down migration
that arrives without a matching up migration.
For each new .up.sql file detected:
1. Reads the .up.sql and its paired .down.sql (if present) from the repo
2. Fetches the review-migration skill from the AI marketplace
3. Calls Claude to produce the schema review report
4. Calls Claude to produce the RST release note draft + changelog summary
5. Appends the combined output to $GITHUB_STEP_SUMMARY so it renders
inline in the Actions run UI — no branch, PR, or extra secrets needed
Required environment variables:
ANTHROPIC_API_KEY — repo secret
GITHUB_STEP_SUMMARY — file path provided automatically by GitHub Actions
"""
import os
import re
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
import anthropic
# ── Config ────────────────────────────────────────────────────────────────────
MODEL = "claude-sonnet-4-6"
# The marketplace URL intentionally points to /main so the script always uses
# the latest version of Ben Cooke's review-migration skill. Any push to
# mattermost-ai-marketplace/main WILL change the skill used by this workflow —
# that is by design. To pin to a specific revision instead, replace "main"
# with a commit SHA (e.g. "/abc1234/plugins/...").
MARKETPLACE_BASE = (
"https://raw.githubusercontent.com/mattermost/mattermost-ai-marketplace"
"/main/plugins/review-migration/skills/review-migration"
)
MM_GUIDE_URL = (
"https://developers.mattermost.com/contribute/more-info/server/schema-migration-guide/"
)
# Retry configuration
MAX_RETRIES = 3
RETRY_BACKOFF_BASE = 2 # seconds; waits 2, 4, 8 between attempts
# Allowlist pattern for migration file paths accepted by this script.
# Format: server/channels/db/migrations/postgres/NNNNNN_slug.up.sql
MIGRATION_PATH_RE = re.compile(
r"^server/channels/db/migrations/postgres/\d{6}_[\w-]+\.up\.sql$"
)
RELEASE_NOTES_SKILL = """
You are helping a Mattermost release manager write database migration release notes.
When given a migration review report, produce two things:
## 1. Release Note Block
Three parts in this exact order:
### Part A — Description
A clear, concise paragraph (2–5 sentences) describing what changed and why.
Write for database admins and self-hosters who need to understand the change at a glance.
Cover what tables/columns/indexes changed, the purpose, and any performance impact.
Do NOT include upgrade instructions here.
Inline code formatting: use DOUBLE backticks for all table names, column names, and
identifiers in prose (RST/Sphinx style). E.g. ``roles``, ``schemeid``, ``permission_level``.
Never use single backticks in description prose.
### Part B — Fixed compatibility statement (copy verbatim every time):
The migrations are fully backwards-compatible and no database downtime is expected for this upgrade. The SQL queries included are:
### Part C — SQL in RST format (NOT markdown fences):
.. code-block:: sql
<SQL here, indented 4 spaces>
For multiple SQL dialects use a separate labeled ``.. code-block:: sql`` block for each.
## 2. One-Line Changelog Summary
**Changelog summary:** `<one sentence under ~30 words, ending with an impact note>`
"""
# ── Startup validation ────────────────────────────────────────────────────────
def validate_env() -> str:
"""
Validate required env vars and return the GITHUB_STEP_SUMMARY path.
Exits with a clear error message if any required var is missing.
ANTHROPIC_API_KEY is validated here so we fail fast with a readable
message rather than an opaque SDK error, but it is not returned —
the Anthropic SDK reads it directly from the environment.
"""
required = {
"ANTHROPIC_API_KEY": "Anthropic API key (repo secret)",
"GITHUB_STEP_SUMMARY": "Job summary file path (provided automatically by Actions)",
}
missing = [
f" {var} ({desc})"
for var, desc in required.items()
if not os.environ.get(var, "").strip()
]
if missing:
print("ERROR: The following required environment variables are not set:\n")
for m in missing:
print(m)
sys.exit(1)
return os.environ["GITHUB_STEP_SUMMARY"]
# ── Input validation ──────────────────────────────────────────────────────────
def validate_migration_paths(paths: list[str]) -> None:
"""
Server-side validation: reject any path that does not match the canonical
migration file pattern. Defense-in-depth against path traversal or
unexpected input if the workflow filter is ever bypassed.
"""
invalid = [p for p in paths if not MIGRATION_PATH_RE.match(p)]
if invalid:
print(
"ERROR: The following paths do not match the expected migration pattern "
f"({MIGRATION_PATH_RE.pattern}):"
)
for p in invalid:
print(f" {p!r}")
print("Aborting — only files under the canonical migrations directory are accepted.")
sys.exit(1)
# ── Retry helper ──────────────────────────────────────────────────────────────
def _is_retryable_http_error(code: int) -> bool:
return code in (429, 500, 502, 503, 504)
def with_retry(fn, *, label: str, retries: int = MAX_RETRIES):
"""Call fn() up to `retries` times with exponential backoff."""
last_exc = None
for attempt in range(1, retries + 1):
try:
return fn()
except urllib.error.HTTPError as e:
last_exc = e
if not _is_retryable_http_error(e.code):
raise
wait = RETRY_BACKOFF_BASE ** attempt
print(f" [{label}] HTTP {e.code} on attempt {attempt}/{retries}. "
f"Retrying in {wait}s…")
time.sleep(wait)
except (urllib.error.URLError, TimeoutError, OSError) as e:
last_exc = e
wait = RETRY_BACKOFF_BASE ** attempt
print(f" [{label}] Network error on attempt {attempt}/{retries}: {e}. "
f"Retrying in {wait}s…")
time.sleep(wait)
except anthropic.RateLimitError as e:
last_exc = e
wait = RETRY_BACKOFF_BASE ** attempt
print(f" [{label}] Anthropic rate limit on attempt {attempt}/{retries}. "
f"Retrying in {wait}s…")
time.sleep(wait)
except anthropic.APIStatusError as e:
last_exc = e
if e.status_code not in (429, 500, 502, 503, 529):
raise
wait = RETRY_BACKOFF_BASE ** attempt
print(f" [{label}] Anthropic API error {e.status_code} on attempt "
f"{attempt}/{retries}. Retrying in {wait}s…")
time.sleep(wait)
raise last_exc
# ── HTTP helpers ──────────────────────────────────────────────────────────────
def fetch_url(url: str) -> str:
def _do():
req = urllib.request.Request(url, headers={"User-Agent": "migration-bot/1.0"})
with urllib.request.urlopen(req, timeout=30) as r:
return r.read().decode()
return with_retry(_do, label=f"GET {url}")
# ── Claude calls ──────────────────────────────────────────────────────────────
def call_claude(system: str, user: str) -> str:
# No explicit api_key — the SDK reads ANTHROPIC_API_KEY from the environment
# automatically, keeping the key out of the call stack and reducing the
# chance of accidental logging.
client = anthropic.Anthropic()
def _do():
msg = client.messages.create(
model=MODEL,
max_tokens=4096,
system=system,
messages=[{"role": "user", "content": user}],
)
return msg.content[0].text
return with_retry(_do, label="Anthropic API")
def run_review(
up_sql: str,
down_sql: str,
skill_md: str,
reference_md: str,
guide_text: str,
) -> str:
system = f"""You are a Mattermost database migration reviewer.
Your instructions (the review-migration skill):
{skill_md}
Supplementary internal reference:
{reference_md}
Official Mattermost DB Migration Guide (source of truth for rules and lock types):
{guide_text[:12000]}
"""
user = f"""Review this migration and produce the full report in the markdown
template format from your instructions.
## Up migration (.up.sql):
```sql
{up_sql}
```
## Down migration (.down.sql):
```sql
{down_sql}
```
"""
return call_claude(system, user)
def run_release_notes(review: str) -> str:
user = f"""Based on this migration review, produce the formatted release note
block and one-line changelog summary.
## Migration Review:
{review}
"""
return call_claude(RELEASE_NOTES_SKILL, user)
# ── Processing ────────────────────────────────────────────────────────────────
def process(
up_path: Path,
skill_md: str,
reference_md: str,
guide_text: str,
) -> str:
"""Run both skills for one migration. Returns the combined markdown."""
up_sql = up_path.read_text(encoding="utf-8")
down_path = up_path.with_name(up_path.name.replace(".up.sql", ".down.sql"))
down_sql = down_path.read_text(encoding="utf-8") if down_path.exists() else "(no down migration)"
print(" Running review-migration skill…")
review = run_review(up_sql, down_sql, skill_md, reference_md, guide_text)
print(" Running migration-release-notes skill…")
release_notes = run_release_notes(review)
return f"{review}\n\n---\n\n## Release Note Draft\n\n{release_notes}"
# ── Job Summary output ────────────────────────────────────────────────────────
def write_summary(summary_path: str, sections: list[tuple[str, str]]) -> None:
"""Append all review sections to the GitHub Actions job summary file."""
with open(summary_path, "a", encoding="utf-8") as f:
f.write("# 🔍 Migration Review & Release Notes\n\n")
for name, content in sections:
f.write(f"## `{name}`\n\n")
f.write(content)
f.write("\n\n---\n\n")
print(f"\nSummary written to job summary ({len(sections)} migration(s)).")
# ── Main ──────────────────────────────────────────────────────────────────────
def main() -> None:
summary_path = validate_env()
new_files = [f for f in sys.argv[1:] if f.strip()]
if not new_files:
print("No new migration files provided. Exiting.")
return
validate_migration_paths(new_files)
print(f"Processing {len(new_files)} migration(s): {new_files}\n")
# Fetch shared resources once
print("Fetching review-migration skill from AI marketplace…")
try:
skill_md = fetch_url(f"{MARKETPLACE_BASE}/SKILL.md")
reference_md = fetch_url(f"{MARKETPLACE_BASE}/reference.md")
except Exception as e:
print(
f"\nERROR: Could not fetch review-migration skill.\n"
f"URL: {MARKETPLACE_BASE}\nReason: {e}"
)
sys.exit(1)
print("Fetching Mattermost DB Migration Guide…")
try:
guide_text = fetch_url(MM_GUIDE_URL)
except Exception as e:
print(f"WARNING: Could not fetch DB Migration Guide ({e}). Proceeding without it.")
guide_text = ""
print()
# Process each migration
sections: list[tuple[str, str]] = []
for filepath in new_files:
up_path = Path(filepath)
print(f"→ {up_path.name}")
try:
content = process(up_path, skill_md, reference_md, guide_text)
except anthropic.AuthenticationError as e:
print(f"\nERROR: Anthropic authentication failed. "
f"Check that ANTHROPIC_API_KEY is valid.\nDetail: {e}")
sys.exit(1)
except Exception as e:
print(f"\nERROR: Failed to process {up_path.name}: {e}")
sys.exit(1)
sections.append((up_path.name, content))
write_summary(summary_path, sections)
if __name__ == "__main__":
main()