Upstream sync #422
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: Upstream sync | |
| # Daily detection of new upstream commits in yuliskov/SmartTube. | |
| # Behaviour: | |
| # - If upstream/master is mergeable into master with no conflicts, push a | |
| # branch `upstream-sync/<date>` and open a PR. | |
| # - If a merge would conflict, open an issue listing the conflicting paths | |
| # so a human can resolve them with the rebase runbook in hand. | |
| # - If master is already up to date with upstream, do nothing. | |
| # | |
| # The actual merge stays a human decision — this workflow only does detection | |
| # and PR/issue creation. See docs/upstream-merge.md for the manual steps. | |
| on: | |
| schedule: | |
| - cron: '17 */6 * * *' # every 6h at :17 (00:17 / 06:17 / 12:17 / 18:17 UTC) | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| sync: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout master | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Add upstream remote | |
| run: | | |
| git remote add upstream https://github.com/yuliskov/SmartTube.git | |
| git fetch upstream master --tags | |
| - name: Configure git identity | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| - name: Skip if an upstream-sync PR or issue is already open | |
| id: existing | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # The REST issues endpoint filters by label server-side and includes PRs | |
| # (PRs are issues in this API). gh's --label flag does a client-side label | |
| # lookup that fails silently under the scoped GITHUB_TOKEN, so use the API. | |
| OPEN=$(gh api "repos/${{ github.repository }}/issues?state=open&labels=upstream-sync" --jq 'length') | |
| echo "open_items=$OPEN" >> $GITHUB_OUTPUT | |
| if [ "$OPEN" != "0" ]; then | |
| echo "An upstream-sync PR or issue is already open ($OPEN item(s)) — skipping this run." | |
| fi | |
| - name: Check for upstream commits | |
| id: check | |
| if: steps.existing.outputs.open_items == '0' | |
| run: | | |
| UPSTREAM_SHA=$(git rev-parse upstream/master) | |
| MERGE_BASE=$(git merge-base master upstream/master) | |
| echo "upstream_sha=$UPSTREAM_SHA" >> $GITHUB_OUTPUT | |
| echo "merge_base=$MERGE_BASE" >> $GITHUB_OUTPUT | |
| if [ "$UPSTREAM_SHA" = "$MERGE_BASE" ]; then | |
| echo "Up to date with upstream — nothing to do." | |
| echo "needs_sync=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "needs_sync=true" >> $GITHUB_OUTPUT | |
| COMMITS=$(git log --oneline "$MERGE_BASE..upstream/master" | head -20) | |
| echo "$COMMITS" > /tmp/upstream-commits.txt | |
| echo "## New upstream commits since last merge" > /tmp/pr-body.md | |
| echo "" >> /tmp/pr-body.md | |
| echo '```' >> /tmp/pr-body.md | |
| cat /tmp/upstream-commits.txt >> /tmp/pr-body.md | |
| echo '```' >> /tmp/pr-body.md | |
| echo "" >> /tmp/pr-body.md | |
| UPSTREAM_TAG=$(git describe --tags --abbrev=0 upstream/master 2>/dev/null || echo "") | |
| if [ -n "$UPSTREAM_TAG" ]; then | |
| echo "Latest upstream tag: \`$UPSTREAM_TAG\`" >> /tmp/pr-body.md | |
| echo "" >> /tmp/pr-body.md | |
| fi | |
| fi | |
| - name: Skip — up to date | |
| if: steps.check.outputs.needs_sync == 'false' | |
| run: echo "Done." | |
| - name: Attempt merge into a sync branch | |
| if: steps.check.outputs.needs_sync == 'true' | |
| id: merge | |
| run: | | |
| DATE=$(date -u +%Y-%m-%d) | |
| # Time suffix so multiple same-day runs (now every 6h) don't collide on the | |
| # no-force push after an earlier sync branch has auto-merged and closed. | |
| BRANCH="upstream-sync/$DATE-$(date -u +%H%M)" | |
| echo "branch=$BRANCH" >> $GITHUB_OUTPUT | |
| git checkout -B "$BRANCH" master | |
| if git merge --no-ff --no-commit upstream/master; then | |
| git commit -m "Merge upstream/master into $BRANCH" | |
| echo "status=clean" >> $GITHUB_OUTPUT | |
| else | |
| CONFLICTS=$(git diff --name-only --diff-filter=U) | |
| echo "$CONFLICTS" > /tmp/conflicts.txt | |
| git merge --abort | |
| echo "status=conflict" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Push sync branch (clean merge) | |
| if: steps.check.outputs.needs_sync == 'true' && steps.merge.outputs.status == 'clean' | |
| run: | | |
| git push origin "${{ steps.merge.outputs.branch }}" | |
| - name: Open PR (clean merge) and enable auto-merge | |
| if: steps.check.outputs.needs_sync == 'true' && steps.merge.outputs.status == 'clean' | |
| env: | |
| # On a fork, the scoped GITHUB_TOKEN is refused `createPullRequest` | |
| # ("Resource not accessible by integration") regardless of the | |
| # default_workflow_permissions=write / can_approve_pull_request_reviews | |
| # toggles — verified live 2026-06-09. A PAT is required for the bot to | |
| # open the PR itself. Prefer SYNC_PAT if configured; otherwise fall back | |
| # to GITHUB_TOKEN, which fails `gh pr create` and trips the never-silent | |
| # issue fallback below so a human can open the (already-pushed) branch. | |
| GH_TOKEN: ${{ secrets.SYNC_PAT || secrets.GITHUB_TOKEN }} | |
| run: | | |
| { | |
| echo "Automatic merge of upstream/master into master — clean, no conflicts." | |
| echo "" | |
| cat /tmp/pr-body.md | |
| echo "" | |
| echo "## Auto-merge" | |
| echo "" | |
| echo "This PR is set to **auto-merge once \`stmobile-validate\` passes** — the check that" | |
| echo "builds the phone APK and confirms the 3 integration points (stmobile flavor blocks +" | |
| echo "the protected \`setupViewManager\`) survive the merge. If validation fails, auto-merge" | |
| echo "is held and the PR waits for a human (see [docs/upstream-merge.md](../blob/master/docs/upstream-merge.md))." | |
| echo "" | |
| echo "_Generated by .github/workflows/upstream-sync.yml_" | |
| } > /tmp/pr-final.md | |
| # Create the PR. If creation fails (e.g. token perms), fall back to an issue so the | |
| # sync is never silently dropped — the branch is already pushed, so a human can PR it. | |
| if ! PR_URL=$(gh pr create \ | |
| --base master \ | |
| --head "${{ steps.merge.outputs.branch }}" \ | |
| --title "Upstream sync $(date -u +%Y-%m-%d) — clean merge" \ | |
| --body-file /tmp/pr-final.md); then | |
| echo "::warning::gh pr create failed — opening a fallback issue instead." | |
| gh api "repos/${{ github.repository }}/issues" \ | |
| -f title="Upstream sync $(date -u +%Y-%m-%d) — PR creation failed, manual merge needed" \ | |
| -f body="The workflow merged \`upstream/master\` into \`${{ steps.merge.outputs.branch }}\` and pushed it, but could not open a PR automatically (token permissions). Open a PR from that branch into master manually." \ | |
| -f 'labels[]=upstream-sync' | |
| exit 0 | |
| fi | |
| # Apply the label via REST (gh's client-side --label lookup fails under the scoped token). | |
| PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$') | |
| gh api "repos/${{ github.repository }}/issues/$PR_NUM/labels" \ | |
| -f 'labels[]=upstream-sync' | |
| # Auto-merge once required checks (stmobile-validate) pass. Held automatically on failure. | |
| gh pr merge "$PR_URL" --auto --merge \ | |
| || echo "::warning::could not enable auto-merge; PR left open for manual merge." | |
| - name: Open issue (conflict) | |
| if: steps.check.outputs.needs_sync == 'true' && steps.merge.outputs.status == 'conflict' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| { | |
| echo "An automatic merge of \`upstream/master\` conflicts with this fork's \`master\`." | |
| echo "" | |
| cat /tmp/pr-body.md | |
| echo "" | |
| echo "## Conflicting paths" | |
| echo "" | |
| echo '```' | |
| cat /tmp/conflicts.txt | |
| echo '```' | |
| echo "" | |
| echo "## Next steps" | |
| echo "" | |
| echo "Follow [docs/upstream-merge.md](../blob/master/docs/upstream-merge.md) for manual resolution. The 3 integration points from the rebase runbook (\`stmobile\` flavor block in \`smarttubetv/build.gradle\` and \`common/build.gradle\`, plus the \`protected setupViewManager\` widening in \`MainApplication\`) are the most likely conflict surfaces." | |
| echo "" | |
| echo "_Generated by .github/workflows/upstream-sync.yml_" | |
| } > /tmp/issue-body.md | |
| # Use the REST API directly — gh issue create --label does a client-side | |
| # label lookup that fails under the scoped GITHUB_TOKEN. | |
| gh api "repos/${{ github.repository }}/issues" \ | |
| -f title="Upstream sync $(date -u +%Y-%m-%d) — manual merge needed" \ | |
| -F body=@/tmp/issue-body.md \ | |
| -f 'labels[]=upstream-sync' \ | |
| -f 'labels[]=conflict' |