Sync upstream release #649
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Sync upstream release | |
| on: | |
| schedule: | |
| - cron: '0 2,8,14,20 * * *' # Every 6h UTC (09, 15, 21, 03 VN) | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Upstream tag to sync (e.g. v9.3.5). Leave empty to auto-detect latest.' | |
| required: false | |
| type: string | |
| permissions: | |
| contents: write | |
| actions: write | |
| pull-requests: write | |
| jobs: | |
| sync: | |
| name: Sync with upstream blockscout/blockscout | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| # PAT required to push .github/workflows/ changes | |
| # GITHUB_TOKEN cannot modify workflow files | |
| token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} | |
| - name: Add upstream remote | |
| run: | | |
| git remote add upstream https://github.com/blockscout/blockscout.git || true | |
| git fetch upstream --tags --force | |
| - name: Determine target tag | |
| id: target | |
| run: | | |
| if [ -n "${{ inputs.tag }}" ]; then | |
| TAG="${{ inputs.tag }}" | |
| else | |
| # Prefer the newest STABLE release (no -alpha/-beta/-rc suffix). | |
| # Fall back to the newest non-alpha prerelease (beta/rc) only when no | |
| # stable release exists. Never auto-sync -alpha tags. | |
| TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | grep -vE -- '-' | head -1) | |
| if [ -z "$TAG" ]; then | |
| TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | grep -ivE -- '-alpha' | head -1) | |
| fi | |
| fi | |
| if [ -z "$TAG" ]; then | |
| echo "::error::No upstream release tag matched 'v[0-9]*.[0-9]*.[0-9]*'. Aborting." | |
| exit 1 | |
| fi | |
| echo "tag=$TAG" >> $GITHUB_OUTPUT | |
| echo "Target tag: $TAG" | |
| # Check if we already synced this tag | |
| MARKER_TAG="synced-$TAG" | |
| if git rev-parse "$MARKER_TAG" >/dev/null 2>&1; then | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| echo "Already synced $TAG, skipping" | |
| else | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| echo "Will sync $TAG" | |
| fi | |
| - name: Merge upstream tag | |
| if: steps.target.outputs.skip == 'false' | |
| id: merge | |
| run: | | |
| TAG="${{ steps.target.outputs.tag }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Files that MUST match upstream exactly (versions, deps, build config). | |
| # Kept in sync with .github/scripts/auto-resolve-merge-conflicts.sh. | |
| VERSION_FILES=( | |
| mix.exs | |
| rel/config.exs | |
| docker/Makefile | |
| CHANGELOG.md | |
| apps/block_scout_web/mix.exs | |
| apps/ethereum_jsonrpc/mix.exs | |
| apps/explorer/mix.exs | |
| apps/indexer/mix.exs | |
| apps/nft_media_handler/mix.exs | |
| apps/utils/mix.exs | |
| ) | |
| # Try clean merge first | |
| if git merge "$TAG" --no-edit -m "Merge upstream $TAG"; then | |
| echo "Clean merge succeeded" | |
| else | |
| # Merge failed - auto-resolve conflicts | |
| echo "::notice::Merge conflict detected, attempting auto-resolve..." | |
| if bash .github/scripts/auto-resolve-merge-conflicts.sh; then | |
| git commit --no-edit -m "Merge upstream $TAG (auto-resolved conflicts)" | |
| else | |
| # Conflicts remain - capture list, abort merge, hand off to PR step. | |
| REMAINING=$(git diff --name-only --diff-filter=U || true) | |
| echo "::warning::Unresolvable conflicts in: $REMAINING" | |
| git merge --abort | |
| echo "result=conflict" >> $GITHUB_OUTPUT | |
| { | |
| echo "conflict_files<<EOF" | |
| echo "$REMAINING" | |
| echo "EOF" | |
| } >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| fi | |
| # Post-merge: ensure version files match upstream EXACTLY | |
| # Git auto-merge can silently pick the wrong side for these files | |
| FIXUP_NEEDED=false | |
| for f in "${VERSION_FILES[@]}"; do | |
| if [ -f "$f" ] && ! diff -q <(git show "$TAG":"$f" 2>/dev/null) "$f" >/dev/null 2>&1; then | |
| echo " Fixup (upstream): $f" | |
| git checkout "$TAG" -- "$f" | |
| FIXUP_NEEDED=true | |
| fi | |
| done | |
| if [ "$FIXUP_NEEDED" = true ]; then | |
| git add -A | |
| git commit -m "fixup: restore upstream version files from $TAG" | |
| echo "::notice::Version files fixed up to match upstream $TAG" | |
| fi | |
| echo "result=success" >> $GITHUB_OUTPUT | |
| - name: Remove upstream-only workflows | |
| if: steps.target.outputs.skip == 'false' && steps.merge.outputs.result == 'success' | |
| run: | | |
| # Keep ONLY our DOS-specific workflows, delete everything else | |
| KEEP_FILES=( | |
| "dependency-build.yml" | |
| "deploy-config.yml" | |
| "publish-regular-docker-image-on-demand.yml" | |
| "sync-upstream.yml" | |
| ) | |
| cd .github/workflows/ | |
| for f in *.yml; do | |
| KEEP=false | |
| for keep in "${KEEP_FILES[@]}"; do | |
| if [ "$f" = "$keep" ]; then | |
| KEEP=true | |
| break | |
| fi | |
| done | |
| if [ "$KEEP" = false ]; then | |
| echo " Removing upstream workflow: $f" | |
| rm "$f" | |
| fi | |
| done | |
| cd ../.. | |
| if git diff --name-only | grep -q '\.github/workflows/'; then | |
| git add .github/workflows/ | |
| git commit -m "chore: remove upstream-only workflows after sync" | |
| fi | |
| - name: Push and tag | |
| if: steps.target.outputs.skip == 'false' && steps.merge.outputs.result == 'success' | |
| run: | | |
| TAG="${{ steps.target.outputs.tag }}" | |
| git push origin HEAD | |
| # Create marker tag so we don't re-sync | |
| git tag "synced-$TAG" | |
| git push origin "synced-$TAG" | |
| # Create the release tag (same as upstream) to trigger docker-publish | |
| git tag "$TAG" --force | |
| git push origin "$TAG" --force | |
| - name: Create release | |
| if: steps.target.outputs.skip == 'false' && steps.merge.outputs.result == 'success' | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT }} | |
| run: | | |
| TAG="${{ steps.target.outputs.tag }}" | |
| # Verify PAT is available (not falling back to GITHUB_TOKEN) | |
| if [ -z "$GH_TOKEN" ]; then | |
| echo "::error::GH_PAT secret is not set. Cannot create release without workflow scope." | |
| exit 1 | |
| fi | |
| # Delete existing release if any (from upstream fork sync) | |
| if RELEASE_ID=$(gh api "repos/${{ github.repository }}/releases/tags/$TAG" --jq '.id' 2>/dev/null); then | |
| echo "Deleting existing release $RELEASE_ID for $TAG" | |
| gh api -X DELETE "repos/${{ github.repository }}/releases/$RELEASE_ID" 2>/dev/null || true | |
| fi | |
| # Create release via API (more reliable than gh release create in Actions) | |
| gh api "repos/${{ github.repository }}/releases" \ | |
| -f tag_name="$TAG" \ | |
| -f name="DOScan $TAG" \ | |
| -f body="Synced from upstream [blockscout/blockscout $TAG](https://github.com/blockscout/blockscout/releases/tag/$TAG)" \ | |
| -f make_latest="true" | |
| - name: Create PR on conflict | |
| if: steps.target.outputs.skip == 'false' && steps.merge.outputs.result == 'conflict' | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT }} | |
| run: | | |
| TAG="${{ steps.target.outputs.tag }}" | |
| BRANCH="sync-upstream-${TAG}" | |
| git checkout -b "$BRANCH" | |
| git merge "$TAG" --no-edit || true | |
| # Auto-resolve what we can. Script exits 1 when conflicts remain; | |
| # that is expected here - we still commit and push the WIP state | |
| # so reviewers see only the unresolvable conflicts. | |
| bash .github/scripts/auto-resolve-merge-conflicts.sh || true | |
| git add -A | |
| git commit -m "WIP: Merge upstream $TAG (has conflicts)" --no-verify || true | |
| git push origin "$BRANCH" --force | |
| EXISTING_PR=$(gh pr list \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --head "$BRANCH" \ | |
| --base main \ | |
| --state open \ | |
| --json url \ | |
| --jq '.[0].url // empty') | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "::notice::Conflict PR already exists: $EXISTING_PR" | |
| exit 0 | |
| fi | |
| # GitHub's GraphQL createPullRequest replica lags behind the REST | |
| # branches endpoint. Even after the branch is visible via REST, the | |
| # GraphQL mutation can return: "No commits between main and BRANCH". | |
| # Retry the PR creation itself with backoff. | |
| REMAINING=$(echo "${{ steps.merge.outputs.conflict_files }}" | head -20) | |
| PR_BODY="$(cat <<EOF | |
| ## Upstream Sync - $TAG | |
| Auto-merge with upstream \`$TAG\` failed. Version/workflow conflicts were auto-resolved, | |
| but the following files have code conflicts that need manual resolution: | |
| \`\`\` | |
| $REMAINING | |
| \`\`\` | |
| **To resolve:** | |
| 1. Check out this branch locally | |
| 2. Resolve remaining conflicts | |
| 3. Push and merge this PR | |
| 4. Then create tag \`$TAG\` to trigger Docker build | |
| [Upstream release notes](https://github.com/blockscout/blockscout/releases/tag/$TAG) | |
| EOF | |
| )" | |
| # Use REST API directly — gh pr create uses GraphQL which has a | |
| # longer replication lag against new branches (>30s observed). | |
| PR_TITLE="Sync upstream $TAG (merge conflicts)" | |
| for attempt in 1 2 3 4 5 6; do | |
| if PR_URL=$(jq -n --arg t "$PR_TITLE" --arg b "$PR_BODY" --arg h "$BRANCH" \ | |
| '{title:$t, body:$b, head:$h, base:"main"}' | \ | |
| gh api -X POST "repos/${GITHUB_REPOSITORY}/pulls" \ | |
| --input - \ | |
| --jq '.html_url' 2>&1); then | |
| echo "PR created successfully on attempt $attempt: $PR_URL" | |
| exit 0 | |
| fi | |
| echo "PR creation failed (attempt $attempt): $PR_URL" | |
| echo "Retrying in 5s..." | |
| sleep 5 | |
| done | |
| echo "::error::PR creation failed after 6 attempts" | |
| exit 1 |