Repository navigation
Update Pangolin CLI #101
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: Update Pangolin CLI | |
| # Checks fosrl/cli for a new release on a schedule. When a newer version exists | |
| # it smoke-tests the new binary against the plugin's assumptions and, if the | |
| # tests pass, rebuilds the package and opens a pull request. If the tests fail | |
| # (the CLI changed in a way that may break the plugin) it opens an issue instead | |
| # of shipping a broken build. | |
| # | |
| # Notes: | |
| # - Opens a PR rather than pushing to main, so you stay in control of releases | |
| # (review + merge/sign yourself). | |
| # - Requires "Allow GitHub Actions to create and approve pull requests" under | |
| # Settings > Actions > General > Workflow permissions. | |
| on: | |
| schedule: | |
| - cron: '0 6 * * *' # daily at 06:00 UTC | |
| workflow_dispatch: | |
| inputs: | |
| force_version: | |
| description: 'Force a specific fosrl/cli tag (leave empty to use the latest release)' | |
| required: false | |
| default: '' | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| update: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Determine current and latest CLI versions | |
| id: ver | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| CURRENT="$(grep -oP '<!ENTITY cliVersion\s+"\K[^"]+' pangolin_cli.plg)" | |
| if [ -n "${{ github.event.inputs.force_version }}" ]; then | |
| LATEST="${{ github.event.inputs.force_version }}" | |
| else | |
| LATEST="$(gh api repos/fosrl/cli/releases/latest --jq '.tag_name')" | |
| fi | |
| echo "current=$CURRENT" >> "$GITHUB_OUTPUT" | |
| echo "latest=$LATEST" >> "$GITHUB_OUTPUT" | |
| if [ "$CURRENT" = "$LATEST" ]; then | |
| echo "update=false" >> "$GITHUB_OUTPUT" | |
| echo "Already on the latest CLI ($CURRENT); nothing to do." | |
| else | |
| echo "update=true" >> "$GITHUB_OUTPUT" | |
| echo "New CLI available: $CURRENT -> $LATEST" | |
| fi | |
| - name: Download new CLI binary | |
| if: steps.ver.outputs.update == 'true' | |
| run: | | |
| set -euo pipefail | |
| URL="https://github.com/fosrl/cli/releases/download/${{ steps.ver.outputs.latest }}/pangolin-cli_linux_amd64" | |
| echo "Downloading $URL" | |
| curl -fL --retry 3 -o pangolin-new "$URL" | |
| chmod +x pangolin-new | |
| - name: Smoke test new binary | |
| id: smoke | |
| if: steps.ver.outputs.update == 'true' | |
| continue-on-error: true # let later steps react to pass/fail via outcome | |
| run: | | |
| set -o pipefail | |
| bash tests/smoke-test.sh ./pangolin-new 2>&1 | tee smoke-results.txt | |
| - name: Open issue on smoke-test failure | |
| if: steps.ver.outputs.update == 'true' && steps.smoke.outcome == 'failure' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| TITLE="Pangolin CLI ${{ steps.ver.outputs.latest }} failed smoke test" | |
| if [ -n "$(gh issue list --state open --search "$TITLE" --json number --jq '.[].number')" ]; then | |
| echo "Issue already open; skipping."; exit 1 | |
| fi | |
| { | |
| echo "A new Pangolin CLI release **${{ steps.ver.outputs.latest }}** is available, but it failed the plugin smoke test, so the package was **not** rebuilt automatically." | |
| echo | |
| echo "The CLI may have changed in a way that breaks the plugin (a required subcommand or flag is missing). Please review." | |
| echo | |
| echo '```' | |
| cat smoke-results.txt | |
| echo '```' | |
| } > issue-body.md | |
| gh issue create --title "$TITLE" --body-file issue-body.md | |
| exit 1 # flag the run as failed so it's visible | |
| # Pull the upstream "What's Changed" notes so the plugin changelog can | |
| # tell users what actually changed in the CLI, not just the version bump. | |
| - name: Collect Pangolin CLI release notes | |
| id: notes | |
| if: steps.ver.outputs.update == 'true' && steps.smoke.outcome == 'success' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| CURRENT="${{ steps.ver.outputs.current }}" | |
| LATEST="${{ steps.ver.outputs.latest }}" | |
| # Every fosrl/cli release newer than the version we currently ship, up | |
| # to and including the one we're bumping to, oldest first. Normally a | |
| # single release, but a missed run or a forced jump can span several. | |
| mapfile -t ALL < <(gh api repos/fosrl/cli/releases --paginate --jq '.[].tag_name' || true) | |
| SELECTED=() | |
| for t in "${ALL[@]:-}"; do | |
| [ -z "$t" ] && continue | |
| [ "$t" = "$CURRENT" ] && continue | |
| # newer than CURRENT and not newer than LATEST? | |
| [ "$(printf '%s\n%s\n' "$CURRENT" "$t" | sort -V | tail -n1)" = "$t" ] || continue | |
| [ "$(printf '%s\n%s\n' "$LATEST" "$t" | sort -V | tail -n1)" = "$LATEST" ] || continue | |
| SELECTED+=("$t") | |
| done | |
| # Fall back to just LATEST if the release list was empty or didn't list it | |
| # (e.g. a forced tag or an API hiccup). | |
| [ ${#SELECTED[@]} -eq 0 ] && SELECTED=("$LATEST") | |
| IFS=$'\n' SELECTED=($(printf '%s\n' "${SELECTED[@]}" | sort -V)); unset IFS | |
| multi=0; [ ${#SELECTED[@]} -gt 1 ] && multi=1 | |
| : > cli-notes.md | |
| for t in "${SELECTED[@]}"; do | |
| BODY="$(gh api "repos/fosrl/cli/releases/tags/$t" --jq '.body' 2>/dev/null || true)" | |
| # Keep only the "What's Changed" bullets; drop container-image lines, | |
| # the Full Changelog link, and any other headings. | |
| NOTES="$(printf '%s\n' "$BODY" \ | |
| | sed -n "/^##[[:space:]]*What.s Changed/,/^##/p" \ | |
| | grep -E '^[[:space:]]*[-*][[:space:]]' \ | |
| | grep -viE 'ghcr\.io|docker\.io|full changelog' \ | |
| | sed -E 's/^[[:space:]]*[-*][[:space:]]*/- /' || true)" | |
| if [ "$multi" = "1" ]; then | |
| printf -- '- CLI %s:\n' "$t" >> cli-notes.md | |
| [ -n "$NOTES" ] && printf '%s\n' "$NOTES" | sed -E 's/^/ /' >> cli-notes.md | |
| else | |
| [ -n "$NOTES" ] && printf '%s\n' "$NOTES" >> cli-notes.md | |
| fi | |
| done | |
| echo "Collected CLI release notes:"; cat cli-notes.md || true | |
| - name: Bump version and rebuild package | |
| id: build | |
| if: steps.ver.outputs.update == 'true' && steps.smoke.outcome == 'success' | |
| run: | | |
| set -euo pipefail | |
| LATEST="${{ steps.ver.outputs.latest }}" | |
| # Same-day collision guard: two upstream releases on one day would | |
| # otherwise produce the same plugin version (and .txz filename) with | |
| # different content/MD5. Unraid compares plugin versions as strings, | |
| # so letter suffixes sort correctly after the bare date. | |
| BASE="$(date -u +%Y.%m.%d)" | |
| NEWVER="$BASE" | |
| SUF=(a b c d e f) | |
| i=0 | |
| while [ -e "packages/pangolin_cli-${NEWVER}.txz" ] || \ | |
| grep -q "###${NEWVER}$" pangolin_cli.plg; do | |
| if [ "$i" -ge "${#SUF[@]}" ]; then | |
| echo "ERROR: exhausted version suffixes for ${BASE}" >&2 | |
| exit 1 | |
| fi | |
| NEWVER="${BASE}${SUF[$i]}" | |
| i=$((i+1)) | |
| done | |
| echo "newver=$NEWVER" >> "$GITHUB_OUTPUT" | |
| # Update the CLI version and the (date-based) plugin version. | |
| sed -i -E "s#(<!ENTITY cliVersion\s+\")[^\"]*(\">)#\1${LATEST}\2#" pangolin_cli.plg | |
| sed -i -E "s#(<!ENTITY version\s+\")[^\"]*(\">)#\1${NEWVER}\2#" pangolin_cli.plg | |
| # Build the changelog entry, folding in the upstream CLI release notes | |
| # collected in the previous step (falls back to a plain bump line if | |
| # none were found). | |
| { | |
| echo "###${NEWVER}" | |
| if [ -s cli-notes.md ]; then | |
| echo "- Updated Pangolin CLI to ${LATEST}. Changes from the Pangolin CLI:" | |
| sed -E 's/^/ /' cli-notes.md | |
| else | |
| echo "- Updated Pangolin CLI to ${LATEST}." | |
| fi | |
| } > changelog-entry.txt | |
| # Prepend the entry just below the <CHANGES> tag. | |
| awk ' | |
| /<CHANGES>/ && !done { | |
| print; print ""; | |
| while ((getline line < "changelog-entry.txt") > 0) print line; | |
| done=1; next | |
| } | |
| { print } | |
| ' pangolin_cli.plg > pangolin_cli.plg.tmp && mv pangolin_cli.plg.tmp pangolin_cli.plg | |
| # Rebuild the .txz and sync its MD5 into the .plg. | |
| bash build.sh | |
| echo "Built package for plugin $NEWVER (CLI $LATEST)" | |
| # No PR is opened unless the rebuilt package is installable (scripts | |
| # executable, root-owned, MD5 in sync with the .plg). | |
| - name: Verify rebuilt package | |
| if: steps.ver.outputs.update == 'true' && steps.smoke.outcome == 'success' | |
| run: bash tests/package-test.sh "packages/pangolin_cli-${{ steps.build.outputs.newver }}.txz" pangolin_cli.plg | |
| - name: Create pull request | |
| if: steps.ver.outputs.update == 'true' && steps.smoke.outcome == 'success' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| LATEST="${{ steps.ver.outputs.latest }}" | |
| NEWVER="${{ steps.build.outputs.newver }}" | |
| BRANCH="automated/update-cli-${LATEST}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git checkout -b "$BRANCH" | |
| # Stage only intended updates/additions; do not stage deletions of older packages. | |
| git add pangolin_cli.plg packages/ | |
| git commit -m "Update Pangolin CLI to ${LATEST} (plugin ${NEWVER})" | |
| git push -f -u origin "$BRANCH" | |
| { | |
| echo "Automated update: **Pangolin CLI ${LATEST}** (plugin version \`${NEWVER}\`)." | |
| echo | |
| if [ -s cli-notes.md ]; then | |
| echo "### Changes from the Pangolin CLI" | |
| echo | |
| cat cli-notes.md | |
| echo | |
| fi | |
| echo "The new binary passed the plugin smoke test:" | |
| echo | |
| echo '```' | |
| cat smoke-results.txt | |
| echo '```' | |
| echo | |
| echo "Review and merge to release. \`packages/pangolin_cli-${NEWVER}.txz\` and the \`pangolin_cli.plg\` MD5 have been rebuilt." | |
| } > pr-body.md | |
| if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then | |
| echo "PR already open for $BRANCH; branch updated."; exit 0 | |
| fi | |
| gh pr create --base main --head "$BRANCH" \ | |
| --title "Update Pangolin CLI to ${LATEST}" \ | |
| --body-file pr-body.md |