Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ jobs:

- name: Ensure lockfiles are unchanged
if: matrix.elixir == '1.13'
run: git diff --exit-code -- mix-1.13-1.14.lock mix-1.15-1.17.lock mix.lock 'test_integrations/*/mix.lock'
run: git diff --exit-code -- mix-1.13-1.14.lock mix-1.15-1.17.lock mix-1.18.lock mix.lock 'test_integrations/*/mix.lock' 'test_integrations/*/mix-*.lock'

- name: Check `mix release` with phoenix_app and sentry configured
if: matrix.release_test
Expand Down
146 changes: 131 additions & 15 deletions .github/workflows/update_lockfiles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,130 @@ permissions:

jobs:
update-lockfiles:
name: Refresh mix.lock files
name: Refresh mix.lock files (${{ matrix.bucket }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
# Each project's mix.exs picks its lockfile based on the running Elixir
# version (see `lockfile/1` in mix.exs), so one Elixir version per
# supported range is needed to refresh every lockfile bucket.
include:
- bucket: "1.13-1.14"
elixir: "1.13"
otp: "25.3"
- bucket: "1.15-1.17"
elixir: "1.17"
otp: "26.2"
- bucket: "1.18"
elixir: "1.18"
otp: "27.2"
- bucket: "default"
elixir: "1.20.2-otp-29"
otp: "29.0.1"

steps:
- name: Check out this repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Setup Elixir and Erlang
uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}

- name: Refresh lockfiles
id: refresh
run: |
if [ "${{ matrix.bucket }}" = "default" ]; then
suffix="mix.lock"
else
suffix="mix-${{ matrix.bucket }}.lock"
fi

# Only refresh lockfiles that already exist and target this bucket -
# not every project has (or needs) a dedicated lockfile per range.
mapfile -t lockfiles < <(git ls-files -- "$suffix" "test_integrations/*/$suffix")

# Parses `mix hex.outdated`'s plain-text table (not --json: two of the
# four matrix buckets run OTP < 27, which --json requires) for rows
# whose Status is exactly "Update possible", printing "<dep> <latest>"
# one per line. The optional "Only" column (test/dev/...) is blank for
# deps used in every env, which shifts field positions, so fields are
# counted from the end of the line (Status is always the last one or
# two words) rather than from the start. Assumes hex.pm's table
# layout; degrades to "no candidates" rather than misparsing if that
# format changes.
list_update_candidates() {
local out
out=$(mix hex.outdated 2>/dev/null) || true
printf '%s\n' "$out" | grep -E 'Update possible[[:space:]]*$' | awk '{print $1, $(NF-2)}'
}

for lock in "${lockfiles[@]}"; do
dir=$(dirname "$lock")
echo "==> Refreshing $lock in $dir"

candidates=$(cd "$dir" && list_update_candidates)
summary_rows=()

if (cd "$dir" && mix deps.update --all && MIX_ENV=test mix compile); then
echo "==> Bulk update succeeded for $lock"
while IFS=' ' read -r dep latest; do
[ -z "$dep" ] && continue
summary_rows+=("| $dep | $latest | bumped (bulk) |")
done <<< "$candidates"
else
echo "==> Bulk update (or its compile check) failed for $lock, falling back to per-dependency updates"
git checkout -- "$lock"
(cd "$dir" && mix deps.get)

good_lock=$(cat "$lock")

while IFS=' ' read -r dep latest; do
[ -z "$dep" ] && continue
echo "----> Trying $dep -> $latest"
if (cd "$dir" && mix deps.update "$dep" && MIX_ENV=test mix compile); then
good_lock=$(cat "$lock")
summary_rows+=("| $dep | $latest | bumped (individually) |")
else
printf '%s\n' "$good_lock" > "$lock"
(cd "$dir" && mix deps.get)
echo "::warning::[$lock] Skipped $dep -> $latest: breaks MIX_ENV=test mix compile on ${{ matrix.bucket }} (Elixir ${{ matrix.elixir }} / OTP ${{ matrix.otp }})"
summary_rows+=("| $dep | $latest | skipped (breaks compile) |")
fi
done <<< "$candidates"
fi

if [ "${#summary_rows[@]}" -gt 0 ]; then
{
echo "### $lock"
echo ""
echo "| Dependency | Latest | Result |"
echo "|---|---|---|"
printf '%s\n' "${summary_rows[@]}"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
fi
done

{
echo "paths<<LOCKFILES_EOF"
printf '%s\n' "${lockfiles[@]}"
echo "LOCKFILES_EOF"
} >> "$GITHUB_OUTPUT"

- name: Upload updated lockfiles
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: lockfiles-${{ matrix.bucket }}
path: ${{ steps.refresh.outputs.paths }}
if-no-files-found: error

create-pr:
name: Create pull request
needs: update-lockfiles
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand All @@ -27,19 +150,11 @@ jobs:
with:
token: ${{ steps.token.outputs.token }}

- name: Setup Elixir and Erlang
uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
- name: Download updated lockfiles
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
elixir-version: "1.18"
otp-version: "27.2"

- name: Refresh lockfiles
run: |
for lock in mix.lock test_integrations/*/mix.lock; do
dir=$(dirname "$lock")
echo "==> Refreshing mix.lock in $dir"
(cd "$dir" && mix deps.update --all)
done
pattern: lockfiles-*
merge-multiple: true

- name: Configure git
run: |
Expand All @@ -51,7 +166,8 @@ jobs:
run: |
# Stage first, then diff the index against HEAD: plain `git diff` ignores
# untracked files, so a brand-new project's lock would look unchanged.
git add mix.lock 'test_integrations/*/mix.lock'
mapfile -t lockfiles < <(git ls-files -- 'mix.lock' 'mix-*.lock' 'test_integrations/*/mix.lock' 'test_integrations/*/mix-*.lock')
git add "${lockfiles[@]}"

if git diff --cached --quiet; then
echo "No lockfile changes; nothing to do."
Expand Down Expand Up @@ -81,7 +197,7 @@ jobs:
script: |
const branchName = process.env.BRANCH_NAME;
const commitTitle = process.env.COMMIT_TITLE;
const prBody = `Automated weekly refresh of the committed \`mix.lock\` files via \`mix deps.update --all\`, keeping dependency pins current with the latest security patches allowed by each project's version constraints.
const prBody = `Automated weekly refresh of the committed \`mix.lock\` files (including the per-Elixir-version-range lockfiles) via \`mix deps.update --all\`, keeping dependency pins current with the latest security patches allowed by each project's version constraints.

#skip-changelog

Expand Down
Loading
Loading