Skip to content

Sync upstream release #175

Sync upstream release

Sync upstream release #175

Workflow file for this run

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
jobs:
sync:
name: Sync with upstream blockscout/blockscout
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
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
# Get latest release tag from upstream (filter only vX.Y.Z pattern)
TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -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)
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..."
for f in "${VERSION_FILES[@]}"; do
if git diff --name-only --diff-filter=U | grep -qx "$f"; then
echo " Auto-resolve (theirs): $f"
git checkout --theirs "$f"
git add "$f"
fi
done
# Accept upstream for ALL upstream workflow files (version bumps)
# Our custom workflows (docker-publish, deploy-config, sync-upstream) won't conflict
CONFLICT_WORKFLOWS=$(git diff --name-only --diff-filter=U | grep '^\.github/workflows/' || true)
if [ -n "$CONFLICT_WORKFLOWS" ]; then
echo " Auto-resolve (theirs): workflow files"
echo "$CONFLICT_WORKFLOWS" | xargs git checkout --theirs
echo "$CONFLICT_WORKFLOWS" | xargs git add
fi
# Check if any conflicts remain
REMAINING=$(git diff --name-only --diff-filter=U || true)
if [ -n "$REMAINING" ]; then
echo "::warning::Unresolvable conflicts in: $REMAINING"
git merge --abort
echo "result=conflict" >> $GITHUB_OUTPUT
echo "conflict_files<<EOF" >> $GITHUB_OUTPUT
echo "$REMAINING" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
exit 0
fi
git commit --no-edit -m "Merge upstream $TAG (auto-resolved conflicts)"
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=(
"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 (same strategy as above)
for f in mix.exs rel/config.exs docker/Makefile CHANGELOG.md apps/*/mix.exs; do
if git diff --name-only --diff-filter=U | grep -qx "$f" 2>/dev/null; then
git checkout --theirs "$f" && git add "$f"
fi
done
CONFLICT_WF=$(git diff --name-only --diff-filter=U | grep '^\.github/workflows/' || true)
[ -n "$CONFLICT_WF" ] && echo "$CONFLICT_WF" | xargs git checkout --theirs && echo "$CONFLICT_WF" | xargs git add
git add -A
git commit -m "WIP: Merge upstream $TAG (has conflicts)" --no-verify || true
git push origin "$BRANCH"
REMAINING=$(echo "${{ steps.merge.outputs.conflict_files }}" | head -20)
gh pr create \
--title "Sync upstream $TAG (merge conflicts)" \
--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
)" \
--head "$BRANCH" \
--base main