Check upstream version #5
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 version | |
| on: | |
| schedule: | |
| - cron: "33 14 * * 3" # weekly, Wednesday ~14:33 UTC — stagger minutes/day per repo to avoid a stampede | |
| workflow_dispatch: {} | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| check-upstream: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Read UPSTREAM.yml | |
| id: manifest | |
| run: | | |
| repo=$(yq '.upstream_repo' UPSTREAM.yml) | |
| strategy=$(yq '.version_strategy' UPSTREAM.yml) | |
| echo "repo=$repo" >> "$GITHUB_OUTPUT" | |
| echo "strategy=$strategy" >> "$GITHUB_OUTPUT" | |
| - name: Get latest upstream version | |
| id: latest | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| repo="${{ steps.manifest.outputs.repo }}" | |
| strategy="${{ steps.manifest.outputs.strategy }}" | |
| if [ "$strategy" = "release" ]; then | |
| tag=$(gh api "repos/$repo/releases/latest" --jq '.tag_name') | |
| else | |
| tag=$(gh api "repos/$repo/tags" --jq '.[0].name') | |
| fi | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| - name: Compare against pinned tags | |
| id: diff | |
| run: | | |
| latest="${{ steps.latest.outputs.tag }}" | |
| changed=0 | |
| baseline="" | |
| count=$(yq '.pinned | length' UPSTREAM.yml) | |
| for i in $(seq 0 $((count - 1))); do | |
| file=$(yq ".pinned[$i].file" UPSTREAM.yml) | |
| kind=$(yq ".pinned[$i].kind" UPSTREAM.yml) | |
| case "$kind" in | |
| from_tag) | |
| current=$(grep -oP '(?<=:)[^\s]+$' <(grep '^FROM' "$file" | tail -1)) | |
| [ -z "$baseline" ] && baseline="$current" | |
| [ "$current" = "$latest" ] || { sed -i "s|:$current|:$latest|" "$file"; changed=1; } | |
| ;; | |
| build_arg) | |
| argname=$(yq ".pinned[$i].arg_name" UPSTREAM.yml) | |
| current=$(grep -oP "(?<=^ARG ${argname}=)[^\s]+$" "$file") | |
| [ -z "$baseline" ] && baseline="$current" | |
| [ "$current" = "$latest" ] || { sed -i "s|ARG ${argname}=${current}|ARG ${argname}=${latest}|" "$file"; changed=1; } | |
| ;; | |
| digest) | |
| # Intentionally not auto-bumped: no upstream version tag to compare against. | |
| # Flagged in the PR body as a manual-check reminder instead. | |
| ;; | |
| *) | |
| echo "::warning::unknown pin kind '$kind' for $file, skipping" ;; | |
| esac | |
| done | |
| echo "changed=$changed" >> "$GITHUB_OUTPUT" | |
| echo "latest=$latest" >> "$GITHUB_OUTPUT" | |
| echo "baseline=$baseline" >> "$GITHUB_OUTPUT" | |
| - name: Classify bump size | |
| id: bump | |
| run: | | |
| strip() { echo "$1" | grep -oP '\d+(\.\d+){0,2}' | head -1; } | |
| old_major=$(strip "${{ steps.diff.outputs.baseline }}" | cut -d. -f1) | |
| new_major=$(strip "${{ steps.diff.outputs.latest }}" | cut -d. -f1) | |
| if [ -n "$old_major" ] && [ -n "$new_major" ] && [ "$old_major" != "$new_major" ]; then | |
| echo "major=1" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "major=0" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Ensure major-version-bump label exists | |
| if: steps.diff.outputs.changed == '1' && steps.bump.outputs.major == '1' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh label create major-version-bump --color B60205 --description "Upstream cut a major release — review breaking changes before merging" 2>/dev/null || true | |
| - name: Open PR | |
| if: steps.diff.outputs.changed == '1' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| branch="upstream-bump/${{ steps.diff.outputs.latest }}" | |
| git checkout -b "$branch" | |
| git config user.name "template-factory-bot" | |
| git config user.email "actions@users.noreply.github.com" | |
| git add -A | |
| git commit -m "chore: bump upstream to ${{ steps.diff.outputs.latest }}" | |
| git push -u origin "$branch" | |
| digest_note="" | |
| if yq -e '.pinned[] | select(.kind == "digest")' UPSTREAM.yml > /dev/null 2>&1; then | |
| digest_note=$'\n\n**Note:** one or more services pin by image digest, not version tag (no upstream version exists to compare) — those were *not* touched by this bump and still need a manual check if you want the latest image.' | |
| fi | |
| title="Bump upstream to ${{ steps.diff.outputs.latest }}" | |
| major_note="" | |
| if [ "${{ steps.bump.outputs.major }}" = "1" ]; then | |
| title="[MAJOR] $title" | |
| major_note=$'\n\n**⚠️ Major version bump** ('"${{ steps.diff.outputs.baseline }}"' → '"${{ steps.diff.outputs.latest }}"$'). Treat this like a new integration, not a routine patch:\n- Re-read the upstream changelog/migration guide for breaking changes (env vars, config shape, DB migrations)\n- Diff against `docs/railway-wiring.md` — service graph, required vars, or volumes may have changed\n- Re-run local `docker compose up` sanity + a smoke deploy before merging; do not fast-merge on CI green alone' | |
| fi | |
| gh pr create --title "$title" \ | |
| --body "Automated version check found a newer upstream release. Review the [upstream changelog](https://github.com/${{ steps.manifest.outputs.repo }}/releases/tag/${{ steps.diff.outputs.latest }}) before merging — this does not run a smoke deploy.${major_note}${digest_note}" \ | |
| --head "$branch" \ | |
| $([ "${{ steps.bump.outputs.major }}" = "1" ] && echo "--label major-version-bump") |