-
Notifications
You must be signed in to change notification settings - Fork 1
Optimize CI: concurrency, path filtering, dynamic matrix, and Windows gating #126
Description
Summary
MonthBar is the #1 CI spender across all BrightDigit repos at $39.98/month (76% of total spend). The highest-impact changes are: adding concurrency groups to cancel stacked runs (clearly happening given 3,064 Linux minutes last month), dynamic matrix gating to limit feature branches, and evaluating/cutting Windows builds ($7.07/month for a macOS menu bar app).
Current cost: $39.98/month ($19.19 Linux + $7.07 Windows + $0.79 claude-code-review.yml). macOS is self-hosted ($0). Estimated savings: $15–19/month.
Changes to make
There is no pre-made replacement workflow file for MonthBar. Read the existing .github/workflows/monthbar.yml first, then apply the patterns below.
1. Add concurrency groups (highest-impact change)
Add at the top level of the workflow (after the on: block):
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueWith 3,064 Linux minutes in March, runs are clearly stacking. This single change may save $5–8/month.
2. Add paths-ignore to push and pull_request triggers
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE'
- '.github/ISSUE_TEMPLATE/**'3. Add a configure job for dynamic matrix gating
Insert this job before build-ubuntu (adjust matrix values to match what's currently in monthbar.yml):
configure:
name: Configure Matrix
runs-on: ubuntu-latest
outputs:
full-matrix: ${{ steps.check.outputs.full }}
cross-platform: ${{ steps.check.outputs.cross_platform }}
ubuntu-os: ${{ steps.matrix.outputs.ubuntu-os }}
ubuntu-swift: ${{ steps.matrix.outputs.ubuntu-swift }}
ubuntu-type: ${{ steps.matrix.outputs.ubuntu-type }}
steps:
- id: check
name: Determine matrix scope
run: |
FULL=false
CROSS_PLATFORM=false
REF="${{ github.ref }}"
EVENT="${{ github.event_name }}"
BASE_REF="${{ github.base_ref }}"
if [[ "$REF" == "refs/heads/main" ]]; then
FULL=true
elif [[ "$REF" =~ ^refs/heads/v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then
FULL=true
elif [[ "$EVENT" == "workflow_dispatch" ]]; then
FULL=true
elif [[ "$EVENT" == "pull_request" ]]; then
if [[ "$BASE_REF" == "main" || "$BASE_REF" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then
FULL=true
CROSS_PLATFORM=true
fi
fi
echo "full=$FULL" >> "$GITHUB_OUTPUT"
echo "cross_platform=$CROSS_PLATFORM" >> "$GITHUB_OUTPUT"
echo "Full matrix: $FULL, Cross-platform: $CROSS_PLATFORM (ref=$REF, event=$EVENT, base_ref=$BASE_REF)"
- id: matrix
name: Build matrix values
run: |
if [[ "${{ steps.check.outputs.full }}" == "true" ]]; then
# Use whatever OS/Swift versions are currently in the full matrix
echo 'ubuntu-os=["noble","jammy"]' >> "$GITHUB_OUTPUT"
echo 'ubuntu-swift=[{"version":"6.2"},{"version":"6.3","nightly":true}]' >> "$GITHUB_OUTPUT"
echo 'ubuntu-type=[""]' >> "$GITHUB_OUTPUT"
else
# Minimal: noble + Swift 6.2 only
echo 'ubuntu-os=["noble"]' >> "$GITHUB_OUTPUT"
echo 'ubuntu-swift=[{"version":"6.2"}]' >> "$GITHUB_OUTPUT"
echo 'ubuntu-type=[""]' >> "$GITHUB_OUTPUT"
fiNote: Adjust the
ubuntu-os,ubuntu-swift, andubuntu-typefull-matrix values to match what's currently inmonthbar.yml. The minimal matrix (noble + Swift 6.2) is standard across repos.
4. Gate build-ubuntu to use dynamic matrix
Update build-ubuntu to depend on configure and use its outputs:
build-ubuntu:
needs: configure
strategy:
matrix:
os: ${{ fromJSON(needs.configure.outputs.ubuntu-os) }}
swift: ${{ fromJSON(needs.configure.outputs.ubuntu-swift) }}
type: ${{ fromJSON(needs.configure.outputs.ubuntu-type) }}Keep all existing steps as-is; only change the needs and strategy.matrix values.
5. Gate Windows builds
MonthBar is a macOS menu bar app — evaluate whether Windows CI provides value. Two options:
Option A (recommended if Windows has no consumers): Remove the Windows job entirely.
Option B (if cross-platform validation is desired): Gate the Windows job to PRs targeting main/semver only:
build-windows:
needs: configure
if: ${{ needs.configure.outputs.cross-platform == 'true' }}
# Keep a single job: windows-2025, Swift 6.2 onlyRead the existing Windows job and decide which option applies. If MonthBar has no Windows/Linux package consumers (it's a menu bar app), Option A saves $7.07/month immediately.
6. Split build-macos into two jobs (if it has a broad matrix)
If build-macos currently includes macOS, tvOS, visionOS, watchOS, and multiple Xcode versions, split it:
build-macos— always runs: SPM + iOS + watchOS (self-hosted, $0)build-macos-platforms— full-matrix only (needs.configure.outputs.full-matrix == 'true'): macOS, tvOS, visionOS, older Xcode versions
If the macOS matrix is already minimal, skip this split.
7. Update the lint job
Change the if condition to:
if: ${{ !cancelled() && !failure() }}And add any new jobs (build-macos-platforms, build-windows if kept) to the needs list.
8. Add .github/workflows/cleanup-caches.yml
Create this new file:
name: Cleanup Branch Caches
on:
delete:
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Cleanup caches for deleted branch
uses: actions/github-script@v7
with:
script: |
const ref = `refs/heads/${context.payload.ref}`;
const caches = await github.rest.actions.getActionsCacheList({
owner: context.repo.owner,
repo: context.repo.repo,
ref: ref,
});
for (const cache of caches.data.actions_caches) {
console.log(`Deleting cache: ${cache.key}`);
await github.rest.actions.deleteActionsCacheById({
owner: context.repo.owner,
repo: context.repo.repo,
cache_id: cache.id,
});
}
console.log(`Deleted ${caches.data.actions_caches.length} cache(s) for ${ref}`);9. Add .github/workflows/cleanup-pr-caches.yml
Create this new file:
name: Cleanup PR Caches
on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Cleanup caches for closed PR
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request.number;
const refs = [
`refs/pull/${pr}/merge`,
`refs/pull/${pr}/head`,
];
for (const ref of refs) {
const caches = await github.rest.actions.getActionsCacheList({
owner: context.repo.owner,
repo: context.repo.repo,
ref,
});
for (const cache of caches.data.actions_caches) {
console.log(`Deleting cache: ${cache.key} (${ref})`);
await github.rest.actions.deleteActionsCacheById({
owner: context.repo.owner,
repo: context.repo.repo,
cache_id: cache.id,
});
}
console.log(`Deleted ${caches.data.actions_caches.length} cache(s) for ${ref}`);
}Impact
- Concurrency: cancels stacked runs — likely the single biggest saving given 3,064 Linux minutes last month
- Dynamic matrix: feature branch pushes drop from ~15+ jobs to ~3–4
- Windows removal/gating: $7.07/month eliminated or reduced to PRs only
- Estimated total savings: $15–19/month (37–47% reduction)
Checklist
- Read
.github/workflows/monthbar.ymlto understand the current job structure - Add
concurrencyblock at the top level - Add
paths-ignoretopushandpull_requesttriggers - Add the
configurejob (adjust full-matrix values to match current Ubuntu matrix) - Update
build-ubuntuto useneeds: configureand dynamic matrix outputs - Decide on Windows: remove entirely (Option A) or gate to cross-platform PRs (Option B)
- Split
build-macosif the current macOS matrix is broad - Update
lintjob condition andneeds - Create
.github/workflows/cleanup-caches.ymlwith the content above - Create
.github/workflows/cleanup-pr-caches.ymlwith the content above - Push to a feature branch and verify minimal jobs run
- Open a PR to
mainand verify full matrix fires