diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5f633be..69c6bc9c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,7 @@ on: branches: - main - net8 + - net9 jobs: check-nuget: @@ -27,7 +28,7 @@ jobs: id: check run: | VERSION="${{ steps.get-version.outputs.version }}" - PACKAGE_IDS=(tickerq tickerq.dashboard tickerq.utilities tickerq.entityframeworkcore tickerq.instrumentation.opentelemetry tickerq.caching.stackexchangeredis) + PACKAGE_IDS=(tickerq tickerq.dashboard tickerq.utilities tickerq.entityframeworkcore tickerq.instrumentation.opentelemetry tickerq.caching.stackexchangeredis tickerq.sdk tickerq.remoteexecutor) MISSING=0 for ID in "${PACKAGE_IDS[@]}"; do @@ -63,7 +64,7 @@ jobs: - name: Setup .NET SDKs uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: 8.0.x - name: Setup Node.js 20 uses: actions/setup-node@v4 @@ -100,6 +101,8 @@ jobs: dotnet build src/TickerQ.Dashboard/TickerQ.Dashboard.csproj --configuration Release dotnet build src/TickerQ.Instrumentation.OpenTelemetry/TickerQ.Instrumentation.OpenTelemetry.csproj --configuration Release dotnet build src/TickerQ.Caching.StackExchangeRedis/TickerQ.Caching.StackExchangeRedis.csproj --configuration Release + dotnet build src/TickerQ.SDK/TickerQ.SDK.csproj --configuration Release + dotnet build src/TickerQ.RemoteExecutor/TickerQ.RemoteExecutor.csproj --configuration Release - name: Pack other projects run: | @@ -108,6 +111,8 @@ jobs: dotnet pack src/TickerQ.Dashboard/TickerQ.Dashboard.csproj --configuration Release --output ./nupkgs dotnet pack src/TickerQ.Instrumentation.OpenTelemetry/TickerQ.Instrumentation.OpenTelemetry.csproj --configuration Release --output ./nupkgs dotnet pack src/TickerQ.Caching.StackExchangeRedis/TickerQ.Caching.StackExchangeRedis.csproj --configuration Release --output ./nupkgs + dotnet pack src/TickerQ.SDK/TickerQ.SDK.csproj --configuration Release --output ./nupkgs + dotnet pack src/TickerQ.RemoteExecutor/TickerQ.RemoteExecutor.csproj --configuration Release --output ./nupkgs - name: Show .nupkg file sizes run: | diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index f362cd84..8b9ce289 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -5,6 +5,7 @@ on: branches: - main - net8 + - net9 jobs: pr: @@ -17,7 +18,7 @@ jobs: - name: Setup .NET SDKs uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: 8.0.x - name: Setup Node.js 20 uses: actions/setup-node@v4 @@ -56,8 +57,10 @@ jobs: dotnet build src/TickerQ.Caching.StackExchangeRedis/TickerQ.Caching.StackExchangeRedis.csproj --configuration Release dotnet build tests/TickerQ.Tests/TickerQ.Tests.csproj --configuration Release dotnet build tests/TickerQ.EntityFrameworkCore.Tests/TickerQ.EntityFrameworkCore.Tests.csproj --configuration Release + dotnet build tests/TickerQ.Caching.StackExchangeRedis.Tests/TickerQ.Caching.StackExchangeRedis.Tests.csproj --configuration Release - name: Run tests run: | dotnet test tests/TickerQ.Tests/ --configuration Release --no-build dotnet test tests/TickerQ.EntityFrameworkCore.Tests/ --configuration Release --no-build + dotnet test tests/TickerQ.Caching.StackExchangeRedis.Tests/ --configuration Release --no-build diff --git a/.github/workflows/sync-version-branches.yml b/.github/workflows/sync-version-branches.yml index d7cd5ac9..27c23adf 100644 --- a/.github/workflows/sync-version-branches.yml +++ b/.github/workflows/sync-version-branches.yml @@ -4,13 +4,12 @@ on: push: branches: - main - - net8 # Allows manual pushes to net8 to be visible, but sync logic only runs from main workflow_dispatch: inputs: target_branches: - description: 'Comma-separated list of target branches (default: net8)' + description: 'Comma-separated list of target branches (default: net8,net9)' required: false - default: 'net8' + default: 'net8,net9' type: string dry_run: description: 'Run without making changes' @@ -23,30 +22,24 @@ jobs: runs-on: ubuntu-latest permissions: - contents: write # Required to push commits to the repository - pull-requests: write # Required to create pull requests - issues: write # Required to create and manage labels + contents: write + pull-requests: write + issues: write - # Only run sync logic when triggered from main branch (prevents infinite loops) if: github.ref == 'refs/heads/main' strategy: matrix: - target_branch: ${{ fromJSON(vars.TARGET_BRANCHES || '["net8"]') }} + target_branch: ${{ fromJSON(vars.TARGET_BRANCHES || '["net8","net9"]') }} fail-fast: false steps: - name: Check PAT_TOKEN exists run: | if [ -z "${{ secrets.PAT_TOKEN }}" ]; then - echo "❌ ERROR: PAT_TOKEN secret is not configured!" - echo "Please add a Personal Access Token with 'repo' scope to repository secrets." - echo "Go to: Settings → Secrets and variables → Actions → New repository secret" - echo "Name: PAT_TOKEN" - echo "Value: Your GitHub Personal Access Token" + echo "::error::PAT_TOKEN secret is not configured" exit 1 fi - echo "✅ PAT_TOKEN is configured" - name: Checkout repository uses: actions/checkout@v4 @@ -59,268 +52,266 @@ jobs: run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" - - # Configure git to use the PAT for authentication git config --global url."https://${{ secrets.PAT_TOKEN }}@github.com/".insteadOf "https://github.com/" - - name: Extract target branches from input - if: github.event_name == 'workflow_dispatch' + - name: Check if sync is needed + id: check run: | - if [ -n "${{ github.event.inputs.target_branches }}" ]; then - echo "TARGET_BRANCHES=[$(echo '${{ github.event.inputs.target_branches }}' | sed 's/,/","/g' | sed 's/^/"/;s/$/"/')]" >> $GITHUB_ENV - fi - - - name: Check if merge is needed - id: check_merge - run: | - echo "🔍 Checking if merge is needed for ${{ matrix.target_branch }}" - - # Check if target branch exists if ! git ls-remote --heads origin ${{ matrix.target_branch }} | grep -q ${{ matrix.target_branch }}; then - echo "branch_exists=false" >> $GITHUB_OUTPUT - echo "❌ Target branch ${{ matrix.target_branch }} does not exist" + echo "needs_sync=false" >> $GITHUB_OUTPUT + echo "::warning::Target branch ${{ matrix.target_branch }} does not exist" exit 0 fi - # Check if main has new commits git checkout main git checkout ${{ matrix.target_branch }} if git merge-base --is-ancestor main ${{ matrix.target_branch }}; then - echo "needs_merge=false" >> $GITHUB_OUTPUT - echo "ℹ️ No new commits to merge" + echo "needs_sync=false" >> $GITHUB_OUTPUT + echo "No new commits to sync" else - echo "needs_merge=true" >> $GITHUB_OUTPUT - echo "✅ New commits found, merge needed" + echo "needs_sync=true" >> $GITHUB_OUTPUT + echo "New commits found, sync needed" fi - - name: Create sync branch and apply changes - if: steps.check_merge.outputs.needs_merge == 'true' + - name: Sync changes + if: steps.check.outputs.needs_sync == 'true' + id: sync run: | - echo "🔄 Creating sync branch from ${{ matrix.target_branch }}" - - # Create a new branch for the PR - sync_branch="sync-main-to-${{ matrix.target_branch }}-$(date +%Y%m%d-%H%M%S)" - git checkout -b "$sync_branch" ${{ matrix.target_branch }} - - # Get ALL commits that need to be applied (including merge commits) - # First, get the merge base to ensure we have the correct starting point - merge_base=$(git merge-base ${{ matrix.target_branch }} main) - echo "📍 Merge base: $merge_base" - - # Get all commits in main that are not in target branch (including merges) - # Store commits in a temp file to avoid subshell issues - git log "$merge_base..main" --format="%H" --reverse > /tmp/commits_to_apply.txt - - # Also get commits using the simpler range (as fallback) - if [ ! -s /tmp/commits_to_apply.txt ]; then - git log "${{ matrix.target_branch }}..main" --format="%H" --reverse > /tmp/commits_to_apply.txt + set -euo pipefail + + TARGET="${{ matrix.target_branch }}" + DOTNET_VER=$(echo "$TARGET" | sed 's/[^0-9]*//g') + + # Create sync branch + sync_branch="sync-main-to-${TARGET}-$(date +%Y%m%d-%H%M%S)" + git checkout -b "$sync_branch" "$TARGET" + + # ── Load .sync-exclude list ── + # These files have version-specific implementations and must stay as-is on the target branch + EXCLUDE_FILES=() + if [ -f ".sync-exclude" ]; then + while IFS= read -r line; do + line=$(echo "$line" | sed 's/#.*//' | xargs) + [ -n "$line" ] && EXCLUDE_FILES+=("$line") + done < .sync-exclude + echo "📋 Loaded ${#EXCLUDE_FILES[@]} files from .sync-exclude" fi - if [ -s /tmp/commits_to_apply.txt ]; then - echo "📋 Found commits to apply:" - while read commit; do - commit_msg=$(git log -1 --format="%s" "$commit") - echo " - $commit: $commit_msg" - done < /tmp/commits_to_apply.txt - - # Track failed commits for reporting - failed_commits_file="/tmp/failed_commits.txt" - touch "$failed_commits_file" - successful_count=0 - failed_count=0 - - # Cherry-pick each commit individually to avoid merge conflicts - while read commit; do - commit_msg=$(git log -1 --format="%s" "$commit") - echo "🍒 Cherry-picking $commit: $commit_msg" - - # Try cherry-pick with conflict resolution - if git cherry-pick --no-commit "$commit" 2>&1; then - echo "✅ Cherry-pick successful for $commit" - successful_count=$((successful_count + 1)) - else - # Check if it's already applied (empty commit) - if git status --porcelain | grep -q "^UU"; then - # Has conflicts - echo "⚠️ Conflicts detected for $commit, will include via merge" - git cherry-pick --abort || true - echo "$commit" >> "$failed_commits_file" - failed_count=$((failed_count + 1)) - elif git diff --cached --quiet && git diff --quiet; then - # Empty commit, already applied - echo "ℹ️ Commit $commit already applied (empty), skipping..." - git cherry-pick --abort || true - else - # Other error, try to continue - echo "⚠️ Cherry-pick had issues for $commit, attempting to continue..." - git add -A || true - if git cherry-pick --continue 2>&1; then - echo "✅ Resolved and applied $commit" - successful_count=$((successful_count + 1)) - else - echo "❌ Could not resolve $commit, will include in merge" - git cherry-pick --abort || true - echo "$commit" >> "$failed_commits_file" - failed_count=$((failed_count + 1)) - fi - fi + # ── Determine commits to cherry-pick ── + if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then + git log "${{ github.event.before }}..${{ github.event.after }}" --format="%H" --reverse > /tmp/commits.txt + else + merge_base=$(git merge-base "$TARGET" main) + git log "$merge_base..main" --format="%H" --reverse > /tmp/commits.txt + fi + + if [ ! -s /tmp/commits.txt ]; then + echo "No commits found" + exit 0 + fi + + # ── Filter already-applied commits using git cherry ── + echo "🔍 Filtering already-applied commits..." + declare -A applied=() + cherry_output=$(git cherry "$TARGET" main 2>/dev/null || echo "") + if [ -n "$cherry_output" ]; then + while IFS= read -r line; do + if [[ $line == -* ]]; then + hash=$(echo "$line" | awk '{print $2}') + [ -n "$hash" ] && applied["$hash"]=1 fi - done < /tmp/commits_to_apply.txt - - # If we have failed commits OR want to ensure all changes are included, do a merge - # This ensures ALL changes from main are included, even if cherry-pick missed some - echo "🔄 Ensuring all changes from main are included..." - - # First, check what files differ between our current state and main - git add -A - current_diff=$(git diff --cached --name-only) - - # Now merge main to get ALL changes (this is a safety net) - if git merge --no-commit --no-ff main -m "Merge main to include all missing commits" 2>&1; then - echo "✅ Merge successful, all commits from main are now included" - else - # Merge has conflicts, resolve by keeping target branch files where specified - echo "⚠️ Merge has conflicts, will resolve by keeping target branch files where needed" - # Conflicts will be resolved in the file reset section below + done <<< "$cherry_output" + fi + + # ── Cherry-pick commits ── + successful=0 + skipped=0 + failed_commits="" + excluded_file_changes="" + + while read -r commit; do + msg=$(git log -1 --format="%s" "$commit" 2>/dev/null || echo "unknown") + + # Skip if already applied (exact hash or patch match) + if git branch --contains "$commit" 2>/dev/null | grep -q "$TARGET"; then + echo "⏭️ Already in branch (exact): $msg" + skipped=$((skipped + 1)) + continue fi - - echo "📊 Summary: $successful_count successful cherry-picks, $failed_count failed (included via merge)" - else - # Even if no commits found via log, check if there are file differences - echo "ℹ️ No commits found via log, checking for file differences..." - - # Check if there are any differences between branches - if git diff --quiet ${{ matrix.target_branch }} main; then - echo "✅ No differences found, branches are in sync" - git checkout main - git branch -D "$sync_branch" 2>/dev/null || true - exit 0 + if [[ -n "${applied[$commit]:-}" ]]; then + echo "⏭️ Already in branch (patch): $msg" + skipped=$((skipped + 1)) + continue + fi + + # Check if this commit touches any excluded files + commit_files=$(git diff-tree --no-commit-id --name-only -r "$commit" 2>/dev/null || true) + for excl in "${EXCLUDE_FILES[@]}"; do + if echo "$commit_files" | grep -qF "$excl"; then + excluded_file_changes="${excluded_file_changes}\n- \`${commit:0:8}\` ($msg) modified \`$excl\`" + fi + done + + # Try cherry-pick + if git cherry-pick --no-commit "$commit" 2>/dev/null; then + if git diff --cached --quiet && git diff --quiet; then + echo "⏭️ Empty after apply: $msg" + git cherry-pick --abort 2>/dev/null || true + skipped=$((skipped + 1)) + else + echo "✅ Applied: $msg" + successful=$((successful + 1)) + fi else - echo "⚠️ Found file differences despite no commits in log" - echo "This might indicate missing changes. Attempting merge to include them..." - - # Try a merge to catch any missing changes - if git merge --no-commit --no-ff main -m "Merge main to include missing changes" 2>&1; then - echo "✅ Merge successful, missing changes included" + # Conflict — accept incoming changes for non-excluded files, keep target for excluded + echo "⚠️ Conflict on: $msg — auto-resolving..." + + # Get list of conflicted files + conflicted_files=$(git diff --name-only --diff-filter=U 2>/dev/null || true) + resolved=true + + for file in $conflicted_files; do + is_excluded=false + for excl in "${EXCLUDE_FILES[@]}"; do + if [ "$file" = "$excl" ]; then + is_excluded=true + break + fi + done + + if [ "$is_excluded" = true ]; then + # Keep target branch version for excluded files + git checkout "$TARGET" -- "$file" 2>/dev/null || true + else + # Accept incoming (main) version for everything else + git checkout --theirs -- "$file" 2>/dev/null || true + fi + git add "$file" 2>/dev/null || true + done + + if [ "$resolved" = true ] && ! git diff --cached --quiet; then + echo "✅ Resolved & applied: $msg" + successful=$((successful + 1)) else - echo "⚠️ Merge has conflicts, will resolve" + echo "❌ Could not resolve: $msg" + git cherry-pick --abort 2>/dev/null || true + failed_commits="${failed_commits}\n- ${commit:0:8}: $msg" fi fi + done < /tmp/commits.txt + + echo "📊 Summary: $successful applied, $skipped skipped" + if [ -n "$failed_commits" ]; then + echo "❌ Failed commits:$failed_commits" fi - - # Reset files that should stay as they are in the target branch - # (This runs for both the if and else branches above) - echo "📝 Processing configuration files..." - - # Reset all .csproj files - find . -name "*.csproj" -type f -exec git checkout ${{ matrix.target_branch }} -- {} \; - - # Get Directory.Build.props from main and update it for net8 + + # ── Reset version-specific files ── + echo "📝 Resetting version-specific files..." + + # 1. Reset all .csproj files to target branch + find . -name "*.csproj" -type f -exec git checkout "$TARGET" -- {} \; 2>/dev/null || true + + # 2. Reset .sync-exclude files to target branch + for excl in "${EXCLUDE_FILES[@]}"; do + if git show "${TARGET}:${excl}" >/dev/null 2>&1; then + git checkout "$TARGET" -- "$excl" + echo " Reset excluded: $excl" + fi + done + + # 3. Update Directory.Build.props from main with correct version numbers if [ -f "src/Directory.Build.props" ]; then - echo "📝 Getting Directory.Build.props from main and updating for ${{ matrix.target_branch }}..." - # Save DotNetAbstractionsVersion from target branch before overwriting target_abstractions_version=$(grep -oP '\K[^<]+' src/Directory.Build.props 2>/dev/null || echo "") - # Explicitly get the file from main branch git checkout main -- src/Directory.Build.props - - # Get the target .NET version - dotnet_version=$(echo "${{ matrix.target_branch }}" | sed 's/[^0-9]*//g') - - # Show current state from main - echo "Directory.Build.props from main (before update):" - grep -E "|" src/Directory.Build.props - - # Update version (e.g. 10.0.1 → 8.0.1) keeping the same minor/patch/suffix + + # Version: e.g. 10.x.x → 8.x.x current_version=$(grep -oP '\K[^<]+' src/Directory.Build.props) - new_version=$(echo "$current_version" | sed "s/^[0-9]\+\./$dotnet_version./") + new_version=$(echo "$current_version" | sed "s/^[0-9]\+\./$DOTNET_VER./") sed -i "s|$current_version|$new_version|g" src/Directory.Build.props - # Update target framework (e.g. net10.0 → net8.0) - sed -i "s|net[0-9]\+\.0|net$dotnet_version.0|g" src/Directory.Build.props + # TargetFramework: e.g. net10.0 → net8.0 + sed -i "s|net[0-9]\+\.0|net${DOTNET_VER}.0|g" src/Directory.Build.props - # Update DotNetVersion range (e.g. [10.0.0,11.0.0) → [8.0.0,9.0.0)) - next_version=$((dotnet_version + 1)) - sed -i "s|\[[0-9]\+\.0\.0,[0-9]\+\.0\.0)|[$dotnet_version.0.0,$next_version.0.0)|g" src/Directory.Build.props - - # Preserve DotNetAbstractionsVersion from target branch (not present on main) + # DotNetVersion: e.g. [10.0.0,11.0.0) → [8.0.0,9.0.0) + next_ver=$((DOTNET_VER + 1)) + sed -i "s|\[[0-9]\+\.0\.0,[0-9]\+\.0\.0)|[${DOTNET_VER}.0.0,${next_ver}.0.0)|g" src/Directory.Build.props + + # Preserve DotNetAbstractionsVersion from target branch if it differs if [ -n "$target_abstractions_version" ]; then - # Insert DotNetAbstractionsVersion after DotNetVersion line - sed -i "//a\\ $target_abstractions_version" src/Directory.Build.props - echo "Preserved DotNetAbstractionsVersion: $target_abstractions_version" + if grep -q '' src/Directory.Build.props; then + sed -i "s|[^<]*|$target_abstractions_version|g" src/Directory.Build.props + else + sed -i "//a\\ $target_abstractions_version" src/Directory.Build.props + fi + echo " Preserved DotNetAbstractionsVersion: $target_abstractions_version" fi - echo "Directory.Build.props after update:" - grep -E "|||" src/Directory.Build.props + echo " Updated Directory.Build.props: Version=$new_version, TF=net${DOTNET_VER}.0, DotNetVersion=[${DOTNET_VER}.0.0,${next_ver}.0.0)" fi - - # Reset workflow files to avoid including workflow changes in the PR - echo "📝 Resetting workflow files to ${{ matrix.target_branch }} version..." + + # 4. Reset workflow files to target branch if [ -d ".github/workflows" ]; then - git checkout ${{ matrix.target_branch }} -- .github/workflows/ || true + git checkout "$TARGET" -- .github/workflows/ 2>/dev/null || true fi - - # Stage all changes + # ── Stage and commit ── git add -A - - # Show what files have changes - echo "📋 Files with changes after cherry-pick and reset:" - git diff --cached --name-status - - # Show actual changes - echo "📝 Actual changes to be committed:" + + echo "📋 Files changed:" git diff --cached --stat - # Check if there are changes to commit if git diff --cached --quiet; then - echo "ℹ️ No changes to commit after processing" - echo "This means all changes from main are already in ${{ matrix.target_branch }}" - echo "or only affected .csproj/Directory.Build.props files which we reset." - git checkout main - git branch -D "$sync_branch" + echo "No changes to commit after processing" exit 0 fi - # Commit the changes - git commit -m "Sync changes from main to ${{ matrix.target_branch }} - Applied recent commits, updated versions & framework, preserved .csproj files" + if [ "${{ inputs.dry_run }}" = "true" ]; then + echo "🏃 Dry run — not committing" + exit 0 + fi + + # Build commit message with failed commits info + COMMIT_MSG="Sync changes from main to ${TARGET}" + if [ -n "$failed_commits" ]; then + COMMIT_MSG="${COMMIT_MSG} + + ⚠️ Commits that could not be applied (need manual review):$(echo -e "$failed_commits")" + fi - echo "📤 Attempting to push sync branch: $sync_branch" - - # Try to push to origin, if it fails try to push to a fork + git commit -m "$COMMIT_MSG" + + # Save excluded file changes for PR comment + if [ -n "$excluded_file_changes" ]; then + { + echo "excluded_file_changes<> $GITHUB_ENV + fi + + # ── Push ── if git push origin "$sync_branch" 2>/dev/null; then - echo "✅ Pushed to origin successfully" - PUSH_REPO="${{ github.repository }}" + echo "sync_branch=$sync_branch" >> $GITHUB_ENV + echo "push_repo=${{ github.repository }}" >> $GITHUB_ENV else - echo "⚠️ Cannot push to origin, trying fork approach..." - - # Check if we have a fork configured FORK_OWNER="${{ vars.FORK_OWNER || 'arcenox' }}" FORK_URL="https://${{ secrets.PAT_TOKEN }}@github.com/${FORK_OWNER}/TickerQ.git" - - # Add fork as remote if not exists git remote add fork "$FORK_URL" 2>/dev/null || git remote set-url fork "$FORK_URL" - - # Push to fork if git push fork "$sync_branch"; then - echo "✅ Pushed to fork: ${FORK_OWNER}/TickerQ" - PUSH_REPO="${FORK_OWNER}/TickerQ" + echo "sync_branch=$sync_branch" >> $GITHUB_ENV + echo "push_repo=${FORK_OWNER}/TickerQ" >> $GITHUB_ENV else - echo "❌ Failed to push to both origin and fork" + echo "::error::Failed to push to both origin and fork" exit 1 fi fi - # Store branch name and repo for PR creation - echo "sync_branch=$sync_branch" >> $GITHUB_ENV - echo "push_repo=$PUSH_REPO" >> $GITHUB_ENV - - name: Ensure labels exist - if: steps.check_merge.outputs.needs_merge == 'true' + if: steps.check.outputs.needs_sync == 'true' && env.sync_branch != '' run: | - # Create labels if they don't exist (ignore errors if they already exist) gh label create "automated" --description "Automated PR" --color "0E8A16" 2>/dev/null || true gh label create "sync" --description "Branch synchronization" --color "1D76DB" 2>/dev/null || true gh label create "${{ matrix.target_branch }}" --description "Target: ${{ matrix.target_branch }} branch" --color "FEF2C0" 2>/dev/null || true @@ -328,89 +319,77 @@ jobs: GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} - name: Create Pull Request - if: steps.check_merge.outputs.needs_merge == 'true' && env.sync_branch != '' + if: steps.check.outputs.needs_sync == 'true' && env.sync_branch != '' run: | - # Create PR using GitHub CLI (gh) - PR_BODY=$(cat << 'EOF' - ## 🤖 Automated Branch Sync + dotnet_version=$(echo "${{ matrix.target_branch }}" | sed 's/[^0-9]*//g') - This PR syncs recent changes from `main` branch to `${{ matrix.target_branch }}`. + PR_BODY=$(cat << 'PREOF' + ## Automated Branch Sync - ### Changes Applied: - - ✅ Applied recent commits from main - - ✅ Updated version numbers (9.0.x → 8.0.x for net8 branch) - - ✅ Updated target framework (net9.0 → net8.0 for net8 branch) - - ✅ Preserved .csproj files from ${{ matrix.target_branch }} branch + This PR syncs recent changes from `main` to `${{ matrix.target_branch }}`. - ### Review Notes: - - All .csproj files maintain ${{ matrix.target_branch }} configurations - - Directory.Build.props has been updated for ${{ matrix.target_branch }} compatibility - - Ready for testing on ${{ matrix.target_branch }} environment + ### What was done: + - Cherry-picked new commits from main + - Updated `Directory.Build.props` versions (10.x.x → ${dotnet_version}.x.x, DotNetVersion range) + - Preserved all `.csproj` files from ${{ matrix.target_branch }} + - Preserved version-specific files listed in `.sync-exclude` + - Skipped commits already in the target branch (patch-level dedup) + + ### Version-specific files (NOT synced): + Files in `.sync-exclude` have different implementations per .NET version and are kept as-is. --- _Created automatically by branch sync workflow_ - EOF + PREOF ) - # Create PR (from fork if necessary) if [ "${{ env.push_repo }}" = "${{ github.repository }}" ]; then - # Creating PR from same repo gh pr create \ --base "${{ matrix.target_branch }}" \ --head "${{ env.sync_branch }}" \ - --title "🔄 Sync main branch changes to ${{ matrix.target_branch }}" \ + --title "Sync main → ${{ matrix.target_branch }}" \ --body "$PR_BODY" \ 2>&1 | tee pr_output.txt else - # Creating PR from fork FORK_OWNER=$(echo "${{ env.push_repo }}" | cut -d'/' -f1) gh pr create \ --base "${{ matrix.target_branch }}" \ --head "${FORK_OWNER}:${{ env.sync_branch }}" \ --repo "${{ github.repository }}" \ - --title "🔄 Sync main branch changes to ${{ matrix.target_branch }}" \ + --title "Sync main → ${{ matrix.target_branch }}" \ --body "$PR_BODY" \ 2>&1 | tee pr_output.txt fi - - # Extract PR number if created + PR_NUMBER=$(grep -oP '(?<=pull/)\d+' pr_output.txt || echo "") - if [ -n "$PR_NUMBER" ]; then - echo "✅ Created PR #$PR_NUMBER" - - # Try to add labels if they exist (ignore errors) - gh pr edit "$PR_NUMBER" --add-label "automated" 2>/dev/null || true - gh pr edit "$PR_NUMBER" --add-label "sync" 2>/dev/null || true - gh pr edit "$PR_NUMBER" --add-label "${{ matrix.target_branch }}" 2>/dev/null || true - - echo "📋 PR URL: https://github.com/${{ github.repository }}/pull/$PR_NUMBER" - else - echo "⚠️ PR might already exist or creation failed. Check if a PR already exists for branch: ${{ env.sync_branch }}" + gh pr edit "$PR_NUMBER" --add-label "automated,sync,${{ matrix.target_branch }}" 2>/dev/null || true + echo "Created PR #$PR_NUMBER: https://github.com/${{ github.repository }}/pull/$PR_NUMBER" + echo "pr_number=$PR_NUMBER" >> $GITHUB_ENV fi env: GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} - - name: Skip notification - if: steps.check_merge.outputs.needs_merge == 'false' - run: echo "ℹ️ No merge needed for ${{ matrix.target_branch }}" + - name: Comment on PR about excluded file changes + if: env.pr_number != '' && env.excluded_file_changes != '' + run: | + COMMENT_BODY=$(cat << 'COMMENTEOF' + ⚠️ **Manual review needed — version-specific files were modified on main** - net8-push-notification: - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/net8' && github.actor == 'github-actions[bot]' + The following commits changed files listed in `.sync-exclude`. These files have different implementations per .NET version and were **not synced** automatically. You may need to manually apply the equivalent changes to the `${{ matrix.target_branch }}` version: - steps: - - name: Notify about bot push to net8 - run: | - echo "## 🤖 Bot Push to net8 Detected" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "This push to net8 branch was made by github-actions[bot] as part of the automated sync from main." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### What happened:" >> $GITHUB_STEP_SUMMARY - echo "- Changes from main were merged into net8" >> $GITHUB_STEP_SUMMARY - echo "- Version numbers updated (9.0.x → 8.0.x)" >> $GITHUB_STEP_SUMMARY - echo "- TargetFramework updated (net9.0 → net8.0)" >> $GITHUB_STEP_SUMMARY - echo "- .csproj files preserved from net8 branch" >> $GITHUB_STEP_SUMMARY + ${{ env.excluded_file_changes }} + + **What to do:** + 1. Review each commit above on `main` + 2. Determine if the change needs an equivalent update for `${{ matrix.target_branch }}` + 3. If so, apply the change manually using the version-specific API (e.g., `SetPropertyCalls` instead of `UpdateSettersBuilder`) + COMMENTEOF + ) + + gh pr comment "${{ env.pr_number }}" --body "$COMMENT_BODY" + env: + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} summary: needs: sync-branches @@ -422,15 +401,8 @@ jobs: run: | echo "## Branch Sync Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - if [ "${{ needs.sync-branches.result }}" = "success" ]; then - echo "✅ All branch sync PRs created successfully" >> $GITHUB_STEP_SUMMARY + echo "All branch syncs completed successfully" >> $GITHUB_STEP_SUMMARY else - echo "❌ Some branch syncs failed" >> $GITHUB_STEP_SUMMARY + echo "Some branch syncs failed — check individual job logs" >> $GITHUB_STEP_SUMMARY fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Next Steps:" >> $GITHUB_STEP_SUMMARY - echo "- Review and merge the created PRs" >> $GITHUB_STEP_SUMMARY - echo "- Test changes on target branches" >> $GITHUB_STEP_SUMMARY - echo "- Target branches: net8 (and any others in TARGET_BRANCHES variable)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/samples/TickerQ.Sample.ApplicationDbContext/TickerQ.Sample.ApplicationDbContext.csproj b/samples/TickerQ.Sample.ApplicationDbContext/TickerQ.Sample.ApplicationDbContext.csproj index e8f0c9d0..4c48e506 100644 --- a/samples/TickerQ.Sample.ApplicationDbContext/TickerQ.Sample.ApplicationDbContext.csproj +++ b/samples/TickerQ.Sample.ApplicationDbContext/TickerQ.Sample.ApplicationDbContext.csproj @@ -1,14 +1,14 @@  - net10.0 + net8.0 enable enable - - + + diff --git a/samples/TickerQ.Sample.Dashboard.ReflectionFree/TickerQ.Sample.Dashboard.ReflectionFree.csproj b/samples/TickerQ.Sample.Dashboard.ReflectionFree/TickerQ.Sample.Dashboard.ReflectionFree.csproj index f7253d0b..cc8a7bdc 100644 --- a/samples/TickerQ.Sample.Dashboard.ReflectionFree/TickerQ.Sample.Dashboard.ReflectionFree.csproj +++ b/samples/TickerQ.Sample.Dashboard.ReflectionFree/TickerQ.Sample.Dashboard.ReflectionFree.csproj @@ -1,14 +1,14 @@ - net10.0 + net8.0 enable enable false - + diff --git a/samples/TickerQ.Sample.WorkerService/TickerQ.Sample.WorkerService.csproj b/samples/TickerQ.Sample.WorkerService/TickerQ.Sample.WorkerService.csproj index 8cdaf3cb..f1700f7e 100644 --- a/samples/TickerQ.Sample.WorkerService/TickerQ.Sample.WorkerService.csproj +++ b/samples/TickerQ.Sample.WorkerService/TickerQ.Sample.WorkerService.csproj @@ -1,14 +1,14 @@ - net10.0 + net8.0 enable enable - - + + diff --git a/setup-branch-sync.sh b/setup-branch-sync.sh deleted file mode 100755 index 8c4950e7..00000000 --- a/setup-branch-sync.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash - -# Setup script for Branch Sync Workflow -# This script helps configure the TARGET_BRANCHES repository variable - -set -e - -echo "🚀 Setting up Branch Sync Workflow" -echo - -# Check if we're in a git repository -if ! git remote get-url origin > /dev/null 2>&1; then - echo "❌ Not in a git repository or no remote origin found" - exit 1 -fi - -# Get repository info -REPO_URL=$(git remote get-url origin) -REPO_NAME=$(basename "$REPO_URL" .git) - -echo "📋 Repository: $REPO_NAME" -echo - -# Default branches -DEFAULT_BRANCHES='["net8"]' - -# Ask user for target branches -echo "Enter target branches (comma-separated, default: net8):" -read -r user_input - -if [ -n "$user_input" ]; then - # Convert comma-separated to JSON array - BRANCHES=$(echo "$user_input" | sed 's/ *, */,/g' | sed 's/,/","/g' | sed 's/^/["/;s/$/"]/') -else - BRANCHES=$DEFAULT_BRANCHES -fi - -echo -echo "🔧 Configuration:" -echo "TARGET_BRANCHES = $BRANCHES" -echo - -# Instructions for manual setup -echo "📝 Manual Setup Instructions:" -echo "1. Go to your repository on GitHub" -echo "2. Navigate to Settings → Secrets and variables → Actions" -echo "3. Click 'Variables' → 'New repository variable'" -echo "4. Name: TARGET_BRANCHES" -echo "5. Value: $BRANCHES" -echo - -echo "Alternatively, you can set it via GitHub CLI:" -echo "gh variable set TARGET_BRANCHES -b '$BRANCHES' -R /$REPO_NAME" -echo - -echo "✅ Setup complete! The workflow will use these branches:" -echo "$BRANCHES" | jq -r '.[]' 2>/dev/null || echo " $(echo "$BRANCHES" | sed 's/["\[\]]//g' | sed 's/,/ /g')" -echo - -echo "💡 Next steps:" -echo "- Push to main branch to trigger the workflow" -echo "- Or manually run the workflow from GitHub Actions tab" \ No newline at end of file diff --git a/src/TickerQ.Caching.StackExchangeRedis/TickerQ.Caching.StackExchangeRedis.csproj b/src/TickerQ.Caching.StackExchangeRedis/TickerQ.Caching.StackExchangeRedis.csproj index 44a89549..749cc711 100644 --- a/src/TickerQ.Caching.StackExchangeRedis/TickerQ.Caching.StackExchangeRedis.csproj +++ b/src/TickerQ.Caching.StackExchangeRedis/TickerQ.Caching.StackExchangeRedis.csproj @@ -1,4 +1,4 @@ - + enable @@ -9,10 +9,7 @@ - - - - + diff --git a/src/TickerQ.Caching.StackExchangeRedis/TickerQRedisContext.cs b/src/TickerQ.Caching.StackExchangeRedis/TickerQRedisContext.cs index 16f602d9..80f409fa 100644 --- a/src/TickerQ.Caching.StackExchangeRedis/TickerQRedisContext.cs +++ b/src/TickerQ.Caching.StackExchangeRedis/TickerQRedisContext.cs @@ -113,8 +113,7 @@ public async Task GetOrSetArrayAsync(string cacheKey, var cachedBytes = await _cache.GetAsync(cacheKey, cancellationToken); if (cachedBytes?.Length > 0) { - ReadOnlySpan cachedSpan = cachedBytes.AsSpan(); - var cached = JsonSerializer.Deserialize(cachedSpan); + var cached = JsonSerializer.Deserialize(cachedBytes); if (cached != null) return cached; diff --git a/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs b/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs index 8ad9d0d4..01eb3b24 100644 --- a/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs +++ b/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs @@ -73,16 +73,16 @@ public async IAsyncEnumerable QueueTimedOutTimeTickers([Enumer var dbContext = session.Context; var context = dbContext.Set(); var now = _clock.UtcNow; - var fallbackThreshold = now.AddMilliseconds(-100); // Fallback picks up tasks overdue by > 100ms + var fallbackThreshold = now.AddSeconds(-1); // Fallback picks up tasks older than main 1-second window var timeTickersToUpdate = await context .AsNoTracking() .Where(x => x.ExecutionTime != null) .Where(x => x.Status == TickerStatus.Idle || x.Status == TickerStatus.Queued) - .Where(x => x.ExecutionTime <= fallbackThreshold) // Only tasks overdue by more than 100ms + .Where(x => x.ExecutionTime <= fallbackThreshold) // Only tasks older than 1 second .Include(x => x.Children.Where(y => y.ExecutionTime == null)) .Select(MappingExtensions.ForQueueTimeTickers()) - .ToArrayAsync(cancellationToken).ConfigureAwait(false);; + .ToArrayAsync(cancellationToken).ConfigureAwait(false); foreach (var timeTicker in timeTickersToUpdate) { @@ -391,7 +391,7 @@ public async IAsyncEnumerable> QueueTime .AsNoTracking() .Include(x => x.CronTicker) .Where(x => x.Status == TickerStatus.Idle || x.Status == TickerStatus.Queued) - .Where(x => x.ExecutionTime <= fallbackThreshold) // Only tasks overdue by more than 100ms + .Where(x => x.ExecutionTime <= fallbackThreshold) // Only tasks older than 1 second .Select(MappingExtensions.ForQueueCronTickerOccurrence, TCronTicker>()) .ToArrayAsync(cancellationToken).ConfigureAwait(false); @@ -551,7 +551,7 @@ public async IAsyncEnumerable> QueueCron public async Task> GetEarliestAvailableCronOccurrence(Guid[] ids, CancellationToken cancellationToken = default) { var now = _clock.UtcNow; - var mainSchedulerThreshold = now.AddMilliseconds(-now.Millisecond); + var mainSchedulerThreshold = now.AddSeconds(-1); var idList = ids.ToList(); using var session = await DbContextFactory.CreateSessionAsync(cancellationToken).ConfigureAwait(false); var dbContext = session.Context; @@ -593,4 +593,4 @@ await dbContext.Set>() } #endregion -} \ No newline at end of file +} diff --git a/src/TickerQ/TickerQ.csproj b/src/TickerQ/TickerQ.csproj index 09bcc17f..ef129326 100644 --- a/src/TickerQ/TickerQ.csproj +++ b/src/TickerQ/TickerQ.csproj @@ -4,7 +4,6 @@ A lightweight, developer-friendly library for queuing and executing cron and time-based jobs in the background. $(PackageTags);background-jobs;task-scheduling TickerQ - latest README.md diff --git a/tests/TickerQ.EntityFrameworkCore.Tests/TickerQ.EntityFrameworkCore.Tests.csproj b/tests/TickerQ.EntityFrameworkCore.Tests/TickerQ.EntityFrameworkCore.Tests.csproj index f3fc1ab2..80618014 100644 --- a/tests/TickerQ.EntityFrameworkCore.Tests/TickerQ.EntityFrameworkCore.Tests.csproj +++ b/tests/TickerQ.EntityFrameworkCore.Tests/TickerQ.EntityFrameworkCore.Tests.csproj @@ -9,7 +9,8 @@ - + + @@ -21,6 +22,7 @@ + diff --git a/tests/TickerQ.Tests/TickerQ.Tests.csproj b/tests/TickerQ.Tests/TickerQ.Tests.csproj index 5cfce2e8..37bb29c7 100644 --- a/tests/TickerQ.Tests/TickerQ.Tests.csproj +++ b/tests/TickerQ.Tests/TickerQ.Tests.csproj @@ -8,7 +8,6 @@ -