Watch Upstream Versions #223
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: Watch Upstream Versions | |
| on: | |
| schedule: | |
| - cron: "17 */6 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| actions: write | |
| contents: write | |
| concurrency: | |
| group: ${{ github.workflow }} | |
| cancel-in-progress: false | |
| jobs: | |
| sync-upstream: | |
| name: Sync Watched Upstream Versions | |
| runs-on: ubuntu-latest | |
| env: | |
| # source: "dockerhub" (default) | "github" | |
| # dockerhub: repo is Docker Hub path (e.g. library/amazonlinux) | |
| # github: repo is owner/name (e.g. MystenLabs/seal); filter matches release tag_name | |
| # required_assets (github only): optional list of asset names that must exist on the release | |
| WATCH_MATRIX: | | |
| [ | |
| { | |
| "package_name": "nitro-cli", | |
| "source": "dockerhub", | |
| "repo": "library/amazonlinux", | |
| "filter": "^(2023[.][0-9]+[.][0-9]+[.][0-9]+)$", | |
| "second_filter": "", | |
| "version_file": "packages/nitro-cli/.current-version", | |
| "publish_input": "deploy-nitro-cli" | |
| }, | |
| { | |
| "package_name": "walrus-upload-relay", | |
| "source": "dockerhub", | |
| "repo": "mysten/walrus-upload-relay", | |
| "filter": "^main-v([0-9]+[.][0-9]+[.][0-9]+)$", | |
| "second_filter": "^main-arm64-v{{version}}$", | |
| "version_file": "packages/walrus-upload-relay/.current-version", | |
| "publish_input": "deploy-walrus-upload-relay" | |
| }, | |
| { | |
| "package_name": "seal-key-server", | |
| "source": "github", | |
| "repo": "MystenLabs/seal", | |
| "filter": "^seal-v([0-9]+[.][0-9]+[.][0-9]+)$", | |
| "required_assets": [ | |
| "key-server-linux-x86_64", | |
| "key-server-linux-aarch64" | |
| ], | |
| "version_file": "packages/seal-key-server/.current-version", | |
| "publish_input": "deploy-seal-key-server" | |
| } | |
| ] | |
| steps: | |
| - name: Checkout main | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| - name: Fetch latest upstream versions | |
| id: watch | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| fetch_latest_dockerhub_tag() { | |
| local repo="$1" | |
| local filter="$2" | |
| local max_pages="$3" | |
| local page=1 | |
| while (( page <= max_pages )); do | |
| response="$( | |
| curl -fsSL "https://hub.docker.com/v2/repositories/${repo}/tags?page=${page}&page_size=100" | |
| )" | |
| tag="$( | |
| jq -r --arg filter "${filter}" ' | |
| .results[]?.name | |
| | match($filter)? | |
| | select(. != null) | |
| | (.captures[0].string // .string) | |
| ' <<< "${response}" | head -n1 | |
| )" | |
| if [[ -n "${tag}" ]]; then | |
| printf '%s\n' "${tag}" | |
| return 0 | |
| fi | |
| page=$((page + 1)) | |
| done | |
| echo "No tag matched ${filter} in Docker Hub repo ${repo}" >&2 | |
| return 1 | |
| } | |
| # Finds the newest non-draft, non-prerelease GitHub release whose tag_name | |
| # matches filter. Prints the first capture group when present, else the full match. | |
| # Optional required_assets_json is a JSON array of asset names that must exist. | |
| fetch_latest_github_release() { | |
| local repo="$1" | |
| local filter="$2" | |
| local max_pages="$3" | |
| local required_assets_json="${4:-[]}" | |
| local auth_args=() | |
| if [[ -n "${GITHUB_TOKEN:-}" ]]; then | |
| auth_args=(-H "Authorization: Bearer ${GITHUB_TOKEN}") | |
| fi | |
| local page=1 | |
| while (( page <= max_pages )); do | |
| response="$( | |
| curl -fsSL \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "${auth_args[@]}" \ | |
| "https://api.github.com/repos/${repo}/releases?per_page=30&page=${page}" | |
| )" | |
| # Empty page means we've exhausted releases. | |
| if [[ "$(jq 'length' <<< "${response}")" -eq 0 ]]; then | |
| break | |
| fi | |
| # Walk releases in API order (newest first). Skip drafts/prereleases. | |
| while read -r release; do | |
| tag_name="$(jq -r '.tag_name // empty' <<< "${release}")" | |
| [[ -z "${tag_name}" ]] && continue | |
| version="$( | |
| jq -rn --arg tag "${tag_name}" --arg filter "${filter}" ' | |
| ($tag | match($filter)?) as $m | |
| | if $m == null then empty | |
| else ($m.captures[0].string // $m.string) | |
| end | |
| ' | |
| )" | |
| [[ -z "${version}" ]] && continue | |
| # Ensure required release assets are present when configured. | |
| missing="$( | |
| jq -r --argjson required "${required_assets_json}" ' | |
| ($required // []) as $need | |
| | .assets // [] | |
| | map(.name) | |
| | . as $have | |
| | $need | |
| | map(select(. as $n | ($have | index($n) | not))) | |
| | .[] | |
| ' <<< "${release}" | |
| )" | |
| if [[ -n "${missing}" ]]; then | |
| echo "Skipping ${repo}@${tag_name}: missing assets: ${missing//$'\n'/, }" >&2 | |
| continue | |
| fi | |
| printf '%s\n' "${version}" | |
| return 0 | |
| done < <(jq -c '.[] | select(.draft == false and .prerelease == false)' <<< "${response}") | |
| page=$((page + 1)) | |
| done | |
| echo "No release matched ${filter} in GitHub repo ${repo}" >&2 | |
| return 1 | |
| } | |
| packages="$(jq -cn '{}')" | |
| while read -r package; do | |
| package_name="$(jq -r '.package_name' <<< "${package}")" | |
| source="$(jq -r '.source // "dockerhub"' <<< "${package}")" | |
| repo="$(jq -r '.repo' <<< "${package}")" | |
| filter="$(jq -r '.filter' <<< "${package}")" | |
| second_filter="$(jq -r '.second_filter // empty' <<< "${package}")" | |
| required_assets="$(jq -c '.required_assets // []' <<< "${package}")" | |
| case "${source}" in | |
| dockerhub) | |
| latest_version="$(fetch_latest_dockerhub_tag "${repo}" "${filter}" 3)" | |
| if [[ -n "${second_filter}" ]]; then | |
| resolved_second_filter="${second_filter//\{\{version\}\}/${latest_version}}" | |
| if ! fetch_latest_dockerhub_tag "${repo}" "${resolved_second_filter}" 3 > /dev/null; then | |
| echo "Skipping ${package_name}: secondary tag check failed for ${resolved_second_filter}" | |
| continue | |
| fi | |
| fi | |
| ;; | |
| github) | |
| latest_version="$(fetch_latest_github_release "${repo}" "${filter}" 5 "${required_assets}")" | |
| ;; | |
| *) | |
| echo "Unknown source '${source}' for package ${package_name}" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| packages="$( | |
| jq -c \ | |
| --arg package_name "${package_name}" \ | |
| --arg latest_version "${latest_version}" \ | |
| --argjson package "${package}" \ | |
| '. + {($package_name): ($package + {latest_version: $latest_version})}' \ | |
| <<< "${packages}" | |
| )" | |
| done < <(jq -c '.[]' <<< "${WATCH_MATRIX}") | |
| { | |
| echo "packages<<EOF" | |
| jq -c . <<< "${packages}" | |
| echo "EOF" | |
| } >> "${GITHUB_OUTPUT}" | |
| - name: Patch and commit latest versions | |
| id: sync | |
| env: | |
| WATCHED_PACKAGES: ${{ steps.watch.outputs.packages }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| update_readme_version() { | |
| local package_name="$1" | |
| local current_version="$2" | |
| local latest_version="$3" | |
| sed -i "/\[\`cmdoss\/${package_name}\`\]/s/\`${current_version}\` 👁️/\`${latest_version}\` 👁️/" README.md | |
| if ! grep -F "[\`cmdoss/${package_name}\`]" README.md | grep -Fq "\`${latest_version}\` 👁️"; then | |
| echo "Failed to update ${package_name} version in README.md" >&2 | |
| exit 1 | |
| fi | |
| } | |
| updated=false | |
| deploy_inputs="$( | |
| jq -c ' | |
| map({(.publish_input): "false"}) | |
| | add | |
| | . + {"deploy-auth-proxy": "false"} | |
| ' <<< "${WATCH_MATRIX}" | |
| )" | |
| summary_file="$(mktemp)" | |
| while read -r package; do | |
| package_name="$(jq -r '.package_name' <<< "${package}")" | |
| version_file="$(jq -r '.version_file' <<< "${package}")" | |
| publish_input="$(jq -r '.publish_input' <<< "${package}")" | |
| latest_version="$(jq -r '.latest_version' <<< "${package}")" | |
| current_version="$(tr -d '[:space:]' < "${version_file}")" | |
| if [[ -z "${current_version}" ]]; then | |
| echo "No version found in ${version_file}" >&2 | |
| exit 1 | |
| fi | |
| if [[ "${current_version}" == "${latest_version}" ]]; then | |
| echo "${package_name} is already at ${latest_version}" | |
| continue | |
| fi | |
| printf '%s\n' "${latest_version}" > "${version_file}" | |
| update_readme_version "${package_name}" "${current_version}" "${latest_version}" | |
| deploy_inputs="$(jq -c --arg publish_input "${publish_input}" '.[$publish_input] = "true"' <<< "${deploy_inputs}")" | |
| updated=true | |
| printf '%s\n' "- ${package_name}: ${current_version} -> ${latest_version}" >> "${summary_file}" | |
| done < <(jq -c '.[]' <<< "${WATCHED_PACKAGES}") | |
| if [[ "${updated}" != "true" ]]; then | |
| { | |
| echo "updated=false" | |
| echo "deploy-inputs<<EOF" | |
| jq -c . <<< "${deploy_inputs}" | |
| echo "EOF" | |
| } >> "${GITHUB_OUTPUT}" | |
| echo "No watched upstream updates found." | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| mapfile -d '' changed_paths < <(git diff --name-only -z) | |
| git add -- "${changed_paths[@]}" | |
| summary="$(cat "${summary_file}")" | |
| git commit -m "chore: update watched upstream versions" -m "${summary}" | |
| git push origin HEAD:main | |
| { | |
| echo "updated=true" | |
| echo "deploy-inputs<<EOF" | |
| jq -c . <<< "${deploy_inputs}" | |
| echo "EOF" | |
| } >> "${GITHUB_OUTPUT}" | |
| - name: Dispatch publish | |
| if: steps.sync.outputs.updated == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| DEPLOY_INPUTS: ${{ steps.sync.outputs['deploy-inputs'] }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| payload="$( | |
| jq -cn \ | |
| --argjson inputs "${DEPLOY_INPUTS}" \ | |
| '{ | |
| ref: "main", | |
| inputs: $inputs | |
| }' | |
| )" | |
| curl -fsSL \ | |
| -X POST \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/workflows/publish.yaml/dispatches" \ | |
| -d "${payload}" |