Compare commits

...
Author SHA1 Message Date
saphidandClaude Opus 5.5 8051687e1e CI: stop recurring false failures on main and pull requests
- checks: the runner's packages.microsoft.com apt source now 403s, failing
  `apt-get update` and with it "Install zsh"; tolerate the refresh error
  (Ubuntu's lists still update and the install still fails if it can't).
- e2e: a failed quiet image build is rebuilt with the full log, but the
  script exited 2 even when that rebuild succeeded; only fail if it fails.
- ArtworkTests: a timed-out lookup gives its resolver slot back from its own
  thread, which a busy macOS runner may schedule after the next test starts.
  Wait for every slot before each test, and always return slots a test took,
  so one late thread can't cascade into three failures.
- release: tagged builds no longer re-upload the installers as workflow
  artifacts (they are attached to the release; the artifact finalize timed
  out on v0.3.0). Pull requests build installers only when the Electron app,
  the macOS helper it compiles, or the workflow changes.
- checks and release: a newer push to a pull request cancels the older run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 21:18:58 +10:00
4 changed files with 54 additions and 13 deletions

No files matched your search

+11 -2
View File
@@ -5,6 +5,11 @@ on:
branches: [main]
pull_request:
# A newer push to the same pull request supersedes a run still in progress.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
checks:
runs-on: ubuntu-latest
@@ -17,7 +22,9 @@ jobs:
with:
node-version: "24"
- name: Install zsh
run: sudo apt-get update -qq && sudo apt-get install -y -qq zsh
# The runner image's third-party apt sources (packages.microsoft.com) sometimes
# fail to refresh; Ubuntu's own lists still update, and the install fails if not.
run: (sudo apt-get update -qq || true) && sudo apt-get install -y -qq zsh
- name: Script syntax
run: |
sh -n ui/local-bin/ssh
@@ -84,6 +91,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install zsh
run: sudo apt-get update -qq && sudo apt-get install -y -qq zsh
# The runner image's third-party apt sources (packages.microsoft.com) sometimes
# fail to refresh; Ubuntu's own lists still update, and the install fails if not.
run: (sudo apt-get update -qq || true) && sudo apt-get install -y -qq zsh
- name: End-to-end tests
run: scripts/e2e.sh
+12 -2
View File
@@ -2,14 +2,22 @@ name: release
# Pushing a v* tag builds Frame Control for macOS, Windows and Linux and attaches
# the installers to that tag's GitHub release (created as a draft if missing).
# Pull requests that touch the app build the same installers as artifacts.
# Pull requests that change how the installers are built (the Electron app, the
# macOS helper it compiles, or this workflow) build the same installers as
# artifacts. ui/, scripts/, frame/ and apk-catalog/ are copied in as they are and
# are tested by the checks workflow; run this one by hand to package them early.
on:
push:
tags: ["v*"]
pull_request:
paths: ["app/**", "ui/**", "scripts/**", "frame/**", "apk-catalog/**", ".github/workflows/release.yml"]
paths: ["app/**", "mac/frame-mac-view/**", ".github/workflows/release.yml"]
workflow_dispatch:
# A newer push to the same pull request supersedes a build still in progress.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: write
@@ -49,7 +57,9 @@ jobs:
tag="${GITHUB_REF_NAME}"
gh release view "$tag" >/dev/null 2>&1 || gh release create "$tag" --draft --title "Frame Control ${tag#v}" --notes ""
gh release upload "$tag" ${{ matrix.files }} --clobber
# Tagged builds are attached to the release above; only other builds need artifacts.
- uses: actions/upload-artifact@v4
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
with:
name: frame-control-${{ matrix.os }}
path: |
+2 -1
View File
@@ -23,7 +23,8 @@ started=$SECONDS
print "==> Building fakeframe-frame (from $base) and fakeframe-host"
# Quiet when it works; if a build fails, build again with the full log so CI shows why.
build() { docker build -q "$@" >/dev/null || { docker build --progress=plain "$@"; exit 2 } }
# That second build also rides out a flaky package mirror: only its failure is fatal.
build() { docker build -q "$@" >/dev/null || docker build --progress=plain "$@" || exit 2 }
build --build-arg BASE="$base" -t fakeframe-frame -f tests/fakeframe/Containerfile tests/fakeframe
build -t fakeframe-host -f tests/fakeframe/host.Containerfile tests/fakeframe
+29 -8
View File
@@ -120,7 +120,27 @@ class LauncherTests(unittest.TestCase):
FIXTURES = ROOT / 'tests/fixtures/library'
def wait_for_idle_resolvers(timeout=10):
# Name lookups that outlast their deadline give their slot back from their own thread,
# which a busy runner may not schedule before the next test; wait until all are back.
from apk_sources import _images
held = []
try:
end = time.monotonic() + timeout
while _images._resolvers.acquire(timeout=max(0, end - time.monotonic())):
held.append(1)
if len(held) == 4:
return
raise AssertionError('artwork name lookups from an earlier test are still running')
finally:
for _ in held:
_images._resolvers.release()
class ArtworkTests(unittest.TestCase):
def setUp(self):
wait_for_idle_resolvers()
def test_source_inputs_and_url(self):
from apk_sources import _images
data = (FIXTURES / 'icon.png').read_bytes()
@@ -231,10 +251,7 @@ class ArtworkTests(unittest.TestCase):
self.assertEqual(sum('Too many' in e for e in errors), 2)
finally:
gate.set()
deadline = time.monotonic() + 5
while time.monotonic() < deadline and not _images._resolvers.acquire(blocking=False):
time.sleep(0.01)
_images._resolvers.release() # the stuck lookups finished and gave their slots back
wait_for_idle_resolvers() # the stuck lookups finished and gave their slots back
def test_resolver_slot_released_when_thread_cannot_start(self):
from apk_sources import _images
@@ -242,10 +259,14 @@ class ArtworkTests(unittest.TestCase):
for _ in range(6):
with self.assertRaises(RuntimeError):
_images.get('https://example.org/a.png', deadline=time.monotonic() + 1)
for _ in range(4): # every slot came back
self.assertTrue(_images._resolvers.acquire(blocking=False))
for _ in range(4):
_images._resolvers.release()
held = 0
try:
while held < 4 and _images._resolvers.acquire(blocking=False):
held += 1
self.assertEqual(held, 4) # every slot came back
finally:
for _ in range(held): # even on failure, so later tests don't inherit the leak
_images._resolvers.release()
def test_deadline_covers_name_resolution(self):
import threading