diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ec0dbee..e7117fde 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,6 +111,12 @@ jobs: GITHUB_SHA: ${{ github.sha }} run: bash tools/ci/architecture_guards.sh + - name: PR Size Guard (advisory) + if: github.event_name == 'pull_request' + env: + GITHUB_BASE_REF: ${{ github.base_ref }} + run: bash tools/ci/pr_size_guard.sh + - name: Test Stability Guards run: bash tools/ci/test_stability_guards.sh diff --git a/.github/workflows/weekly-release.yml b/.github/workflows/weekly-release.yml new file mode 100644 index 00000000..860aa88d --- /dev/null +++ b/.github/workflows/weekly-release.yml @@ -0,0 +1,104 @@ +name: weekly-release + +on: + # Every Friday at 15:00 UTC (23:00 CST) + schedule: + - cron: "0 15 * * 5" + workflow_dispatch: + inputs: + skip_ci_check: + description: "Skip CI green check (emergency release)" + type: boolean + default: false + +concurrency: + group: weekly-release + cancel-in-progress: false + +jobs: + weekly-tag: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + ref: dev + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check for new commits since last tag + id: check + run: | + LAST_TAG=$(git tag --sort=-creatordate | head -1) + if [ -z "$LAST_TAG" ]; then + echo "No previous tags found. Will create baseline." + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "version=v0.1.0" >> "$GITHUB_OUTPUT" + else + COMMITS_SINCE=$(git rev-list --count "${LAST_TAG}..HEAD") + echo "Commits since ${LAST_TAG}: ${COMMITS_SINCE}" + if [ "$COMMITS_SINCE" -eq 0 ]; then + echo "No new commits. Skipping." + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + # Bump patch version + CURRENT=$(echo "$LAST_TAG" | sed 's/^v//') + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + NEW_PATCH=$((PATCH + 1)) + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "version=v${MAJOR}.${MINOR}.${NEW_PATCH}" >> "$GITHUB_OUTPUT" + fi + fi + + - name: Verify CI is green on dev + if: steps.check.outputs.has_changes == 'true' && !inputs.skip_ci_check + run: | + # Check the latest commit on dev has passing status + STATUS=$(gh api repos/${{ github.repository }}/commits/dev/status --jq '.state' 2>/dev/null || echo "unknown") + if [ "$STATUS" != "success" ] && [ "$STATUS" != "unknown" ]; then + echo "::error::CI is not green on dev (status: ${STATUS}). Fix CI before releasing." + echo "::error::Use workflow_dispatch with skip_ci_check=true for emergency releases." + exit 1 + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Merge dev into master and tag + if: steps.check.outputs.has_changes == 'true' + run: | + VERSION="${{ steps.check.outputs.version }}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git checkout master + git merge dev --no-edit --ff-only || { + echo "::error::Cannot fast-forward master to dev. Manual merge required." + exit 1 + } + + git tag -a "${VERSION}" -m "Weekly release ${VERSION}" + git push origin master --tags + + echo "Tagged ${VERSION} on master." + + - name: Create GitHub Release + if: steps.check.outputs.has_changes == 'true' + run: | + VERSION="${{ steps.check.outputs.version }}" + LAST_TAG=$(git tag --sort=-creatordate | sed -n '2p') + + if [ -n "$LAST_TAG" ]; then + BODY=$(git log --oneline "${LAST_TAG}..${VERSION}" | head -30) + else + BODY="Baseline release." + fi + + gh release create "${VERSION}" \ + --title "${VERSION}" \ + --notes "${BODY}" \ + --target master + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/decisions/0007-stream-forward.md b/docs/decisions/0007-stream-forward.md index cfdcf799..3c1b2b6e 100644 --- a/docs/decisions/0007-stream-forward.md +++ b/docs/decisions/0007-stream-forward.md @@ -1,6 +1,3 @@ -<<<<<<<< HEAD:docs/history/2026-03/STREAM_FORWARD_ARCHITECTURE.md -# Aevatar Stream Forward 架构说明 -======== --- title: "Aevatar Stream Forward 架构说明(2026-02-22)" status: active @@ -8,7 +5,6 @@ owner: eanzhao --- # Aevatar Stream Forward 架构说明(2026-02-22) ->>>>>>>> c20fc87ec173e49be645ea287f4bb54ecd975935:docs/decisions/0007-stream-forward.md > Last updated: 2026-04-03. Active runtime paths: `InMemory` (dev/test) and `Orleans` with `KafkaProvider` (production). diff --git a/tools/ci/pr_size_guard.sh b/tools/ci/pr_size_guard.sh new file mode 100755 index 00000000..6f7a903d --- /dev/null +++ b/tools/ci/pr_size_guard.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# PR Size Guard — warns when a PR exceeds the file/LOC thresholds. +# Runs in CI on pull_request events. Advisory only (does not block merge). +# +# Thresholds: +# FILES_THRESHOLD=30 (max changed files, excluding auto-generated) +# LOC_THRESHOLD=800 (max net lines changed) +# +# Auto-generated files excluded from count: +# *.Designer.cs, *.g.cs, **/obj/**, **/bin/**, **/*.pb.cs + +set -euo pipefail + +FILES_THRESHOLD="${PR_SIZE_FILES_THRESHOLD:-30}" +LOC_THRESHOLD="${PR_SIZE_LOC_THRESHOLD:-800}" + +# Determine diff target +if [[ -n "${GITHUB_BASE_REF:-}" ]]; then + git fetch --no-tags --depth=1 origin "${GITHUB_BASE_REF}" 2>/dev/null || true + DIFF_TARGET="origin/${GITHUB_BASE_REF}...HEAD" +elif [[ -n "${1:-}" ]]; then + DIFF_TARGET="$1" +else + DIFF_TARGET="origin/dev...HEAD" +fi + +echo "PR Size Guard: comparing ${DIFF_TARGET}" +echo "Thresholds: files=${FILES_THRESHOLD}, LOC=${LOC_THRESHOLD}" + +# Count changed files (excluding auto-generated) +CHANGED_FILES=$(git diff --name-only "${DIFF_TARGET}" 2>/dev/null \ + | grep -v '\.Designer\.cs$' \ + | grep -v '\.g\.cs$' \ + | grep -v '\.pb\.cs$' \ + | grep -v '/obj/' \ + | grep -v '/bin/' \ + | wc -l | tr -d ' ') + +# Count net LOC changed +STAT_LINE=$(git diff --stat "${DIFF_TARGET}" 2>/dev/null | tail -1) +INSERTIONS=$(echo "${STAT_LINE}" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0") +DELETIONS=$(echo "${STAT_LINE}" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0") +NET_LOC=$(( INSERTIONS - DELETIONS )) +# Use absolute value for comparison +ABS_LOC=${NET_LOC#-} + +echo "Result: ${CHANGED_FILES} files changed, +${INSERTIONS}/-${DELETIONS} (net ${NET_LOC})" + +EXIT_CODE=0 + +if [[ "${CHANGED_FILES}" -gt "${FILES_THRESHOLD}" ]]; then + echo "" + echo "::warning::PR exceeds file threshold: ${CHANGED_FILES} files changed (limit: ${FILES_THRESHOLD}). Consider splitting this PR." + EXIT_CODE=1 +fi + +if [[ "${ABS_LOC}" -gt "${LOC_THRESHOLD}" ]]; then + echo "" + echo "::warning::PR exceeds LOC threshold: ${ABS_LOC} net lines (limit: ${LOC_THRESHOLD}). Consider splitting this PR." + EXIT_CODE=1 +fi + +if [[ "${EXIT_CODE}" -eq 0 ]]; then + echo "PR size OK." +fi + +# Advisory only — always exit 0 so we don't block merge. +# To make this blocking, change the line below to: exit ${EXIT_CODE} +exit 0 diff --git a/tools/ci/stale_branch_cleanup.sh b/tools/ci/stale_branch_cleanup.sh new file mode 100755 index 00000000..678c2907 --- /dev/null +++ b/tools/ci/stale_branch_cleanup.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Stale Branch Cleanup — deletes remote branches that are already merged into dev. +# Safety: ONLY deletes branches whose commits are fully reachable from dev. +# Protected branches (master, dev, main, release/*) are never deleted. +# +# Usage: +# bash tools/ci/stale_branch_cleanup.sh # dry-run (default) +# bash tools/ci/stale_branch_cleanup.sh --apply # actually delete + +set -euo pipefail + +DRY_RUN=true +if [[ "${1:-}" == "--apply" ]]; then + DRY_RUN=false +fi + +TARGET_BRANCH="${CLEANUP_TARGET_BRANCH:-dev}" + +echo "Stale Branch Cleanup" +echo "Target: origin/${TARGET_BRANCH}" +echo "Mode: $(${DRY_RUN} && echo 'DRY RUN' || echo 'APPLY')" +echo "" + +git fetch --prune origin 2>/dev/null || true + +# Protected branch patterns — never delete these +PROTECTED_PATTERN="^origin/(master|dev|main|release/)" + +MERGED_BRANCHES=$(git branch -r --merged "origin/${TARGET_BRANCH}" 2>/dev/null \ + | sed 's/^ *//' \ + | grep '^origin/' \ + | grep -v "origin/HEAD" \ + | grep -Ev "${PROTECTED_PATTERN}" \ + || true) + +if [[ -z "${MERGED_BRANCHES}" ]]; then + echo "No stale merged branches found." + exit 0 +fi + +COUNT=$(echo "${MERGED_BRANCHES}" | wc -l | tr -d ' ') +echo "Found ${COUNT} merged branches:" +echo "" + +while IFS= read -r branch; do + REMOTE_NAME="${branch#origin/}" + LAST_COMMIT_DATE=$(git log -1 --format="%ai" "${branch}" 2>/dev/null | cut -d' ' -f1) + LAST_AUTHOR=$(git log -1 --format="%an" "${branch}" 2>/dev/null) + + if ${DRY_RUN}; then + echo " [DRY RUN] would delete: ${REMOTE_NAME} (last: ${LAST_COMMIT_DATE} by ${LAST_AUTHOR})" + else + echo " Deleting: ${REMOTE_NAME} (last: ${LAST_COMMIT_DATE} by ${LAST_AUTHOR})" + git push origin --delete "${REMOTE_NAME}" 2>/dev/null || echo " Failed to delete ${REMOTE_NAME}" + fi +done <<< "${MERGED_BRANCHES}" + +echo "" +if ${DRY_RUN}; then + echo "Dry run complete. Run with --apply to delete." +else + echo "Cleanup complete. ${COUNT} branches deleted." +fi