Skip to content

Update Pangolin CLI #109

Update Pangolin CLI

Update Pangolin CLI #109

Workflow file for this run

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