Check Upstream Changes #3
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: Check Upstream Changes | |
| on: | |
| schedule: | |
| - cron: '0 9 * * *' # Daily at 09:00 UTC | |
| workflow_dispatch: # Manual trigger | |
| permissions: | |
| contents: read | |
| issues: write | |
| jobs: | |
| check-upstream: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout with submodule | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: true | |
| fetch-depth: 0 | |
| - name: Fetch upstream develop | |
| run: git -C upstream fetch origin develop | |
| - name: Compare submodule to upstream develop | |
| id: compare | |
| run: | | |
| CURRENT=$(git -C upstream rev-parse HEAD) | |
| LATEST=$(git -C upstream rev-parse origin/develop) | |
| CURRENT_SHORT=${CURRENT:0:8} | |
| LATEST_SHORT=${LATEST:0:8} | |
| echo "current=$CURRENT" >> "$GITHUB_OUTPUT" | |
| echo "latest=$LATEST" >> "$GITHUB_OUTPUT" | |
| echo "current_short=$CURRENT_SHORT" >> "$GITHUB_OUTPUT" | |
| echo "latest_short=$LATEST_SHORT" >> "$GITHUB_OUTPUT" | |
| if [ "$CURRENT" = "$LATEST" ]; then | |
| echo "Submodule is up to date with upstream develop." | |
| echo "up_to_date=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Submodule is behind upstream develop ($CURRENT_SHORT → $LATEST_SHORT)." | |
| echo "up_to_date=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Diff watched paths | |
| id: diff | |
| if: steps.compare.outputs.up_to_date == 'false' | |
| run: | | |
| CURRENT=${{ steps.compare.outputs.current }} | |
| LATEST=${{ steps.compare.outputs.latest }} | |
| CHANGED=$(git -C upstream diff --name-only "$CURRENT" "$LATEST" -- \ | |
| "booklore-api/src/main/resources/db/migration/" \ | |
| "booklore-api/src/main/java/org/booklore/model/entity/" \ | |
| "booklore-api/src/main/java/org/booklore/repository/") | |
| if [ -z "$CHANGED" ]; then | |
| echo "No changes in watched paths." | |
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Changes detected in watched paths." | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| echo "$CHANGED" > /tmp/changed_files.txt | |
| git -C upstream diff "$CURRENT" "$LATEST" -- \ | |
| "booklore-api/src/main/resources/db/migration/" \ | |
| "booklore-api/src/main/java/org/booklore/model/entity/" \ | |
| "booklore-api/src/main/java/org/booklore/repository/" \ | |
| > /tmp/upstream.patch | |
| fi | |
| - name: Check for existing open issue | |
| id: existing | |
| if: steps.diff.outputs.has_changes == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| script: | | |
| const sha = '${{ steps.compare.outputs.latest_short }}'; | |
| const { data: issues } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'upstream-sync', | |
| }); | |
| const duplicate = issues.find(i => i.title.includes(sha)); | |
| return duplicate ? String(duplicate.number) : ''; | |
| - name: Ensure upstream-sync label exists | |
| if: steps.diff.outputs.has_changes == 'true' && steps.existing.outputs.result == '' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| try { | |
| await github.rest.issues.getLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: 'upstream-sync', | |
| }); | |
| } catch (e) { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: 'upstream-sync', | |
| color: '0075ca', | |
| description: 'Upstream Booklore changes that need to be ported', | |
| }); | |
| } | |
| - name: Create issue | |
| if: steps.diff.outputs.has_changes == 'true' && steps.existing.outputs.result == '' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const currentShort = '${{ steps.compare.outputs.current_short }}'; | |
| const latestShort = '${{ steps.compare.outputs.latest_short }}'; | |
| const upstreamUrl = 'https://github.com/booklore-app/booklore'; | |
| const changedFiles = fs.readFileSync('/tmp/changed_files.txt', 'utf8') | |
| .trim().split('\n').filter(Boolean); | |
| const migrations = changedFiles.filter(f => f.includes('/db/migration/')); | |
| const entities = changedFiles.filter(f => f.includes('/model/entity/')); | |
| const repositories = changedFiles.filter(f => f.includes('/repository/')); | |
| let patch = fs.readFileSync('/tmp/upstream.patch', 'utf8'); | |
| const MAX = 50_000; | |
| if (patch.length > MAX) { | |
| patch = patch.slice(0, MAX) + | |
| '\n\n… diff truncated. ' + | |
| `[View full diff on GitHub](${upstreamUrl}/compare/${currentShort}...${latestShort})`; | |
| } | |
| const fileList = (files) => | |
| files.map(f => `- \`${f.split('/').pop()}\``).join('\n'); | |
| let body = `## Upstream changes detected\n\n`; | |
| body += `The upstream \`develop\` branch has moved ahead of our pinned submodule commit.\n\n`; | |
| body += `| | Commit |\n|---|---|\n`; | |
| body += `| **Submodule (current)** | [\`${currentShort}\`](${upstreamUrl}/commit/${currentShort}) |\n`; | |
| body += `| **Upstream develop (latest)** | [\`${latestShort}\`](${upstreamUrl}/commit/${latestShort}) |\n`; | |
| body += `| **Full diff** | [Compare on GitHub](${upstreamUrl}/compare/${currentShort}...${latestShort}) |\n\n`; | |
| if (migrations.length > 0) { | |
| body += `### 🗄️ New / changed Flyway migrations (${migrations.length})\n\n`; | |
| body += `Port these to Liquibase in \`src/main/resources/db/changelog/changelogs/\`.\n\n`; | |
| body += fileList(migrations) + '\n\n'; | |
| } | |
| if (entities.length > 0) { | |
| body += `### 🏗️ Changed entities (${entities.length})\n\n`; | |
| body += `Review for new fields that may require Liquibase columns or \`\${datetime.type}\` / \`\${id.type}\` substitutions.\n\n`; | |
| body += fileList(entities) + '\n\n'; | |
| } | |
| if (repositories.length > 0) { | |
| body += `### 🔍 Changed repositories (${repositories.length})\n\n`; | |
| body += `Check for new \`nativeQuery = true\` annotations — these need JPQL rewrites in our overlay for SQLite/PostgreSQL compatibility.\n\n`; | |
| body += fileList(repositories) + '\n\n'; | |
| } | |
| body += `### ✅ Checklist\n\n`; | |
| body += `- [ ] Update submodule to \`${latestShort}\` (or the latest stable tag)\n`; | |
| if (migrations.length > 0) | |
| body += `- [ ] Port ${migrations.length} new Flyway migration(s) to Liquibase changelogs\n`; | |
| if (entities.length > 0) | |
| body += `- [ ] Review ${entities.length} changed entity file(s) for schema impact\n`; | |
| if (repositories.length > 0) | |
| body += `- [ ] Audit ${repositories.length} changed repository file(s) for new native queries\n`; | |
| body += `- [ ] Diff upstream \`build.gradle\` for dependency changes\n`; | |
| body += `- [ ] Build and run full test suite (\`./gradlew test\`)\n\n`; | |
| body += `<details>\n<summary>Full diff (watched paths only)</summary>\n\n`; | |
| body += `\`\`\`diff\n${patch}\n\`\`\`\n</details>\n`; | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `Upstream sync required: ${latestShort}`, | |
| body, | |
| assignees: ['binarymelon'], | |
| labels: ['upstream-sync'], | |
| }); | |
| console.log(`Issue created for upstream commit ${latestShort}`); |