Skip to content
Merged
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
20 changes: 13 additions & 7 deletions .github/workflows/docker-gradle-build-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,16 @@ jobs:
org.opencontainers.image.vendor=Grounds
org.opencontainers.image.ref.name=${{ github.sha }}

# Repository-scoped GHA cache survives ephemeral ARC runners without
# exposing intermediate build layers in a potentially public GHCR
# package. PRs never write cache or image tags. Cache mounts (e.g. Gradle
# dependency mounts) are not exported; the first builds may be colder.
# The GHA layer cache is used on GitHub-hosted runners only. From the
# self-hosted ARC runners it downloads at tens of KB/s: plugin-match
# 0.9.0 (2026-09-29) spent 20+ minutes pulling one 93 MB cached layer at
# ~60 KB/s for a build that takes a few minutes uncached — and the
# 60-minute ARC timeout below exists for the same symptom. On ARC the
# build runs without a layer cache instead. On hosted runners the
# repository-scoped cache still saves time without exposing intermediate
# layers in a potentially public GHCR package. PRs never write cache or
# image tags. Cache mounts (e.g. Gradle dependency mounts) are not
# exported either way.
- name: Build image (pull requests)
if: github.event_name == 'pull_request'
uses: docker/build-push-action@v7
Expand All @@ -142,7 +148,7 @@ jobs:
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ github.repository }}-amd64
cache-from: ${{ runner.environment == 'github-hosted' && format('type=gha,scope={0}-amd64', github.repository) || '' }}
build-args: |
GITHUB_USER=${{ github.actor }}
secrets: |
Expand All @@ -158,8 +164,8 @@ jobs:
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ github.repository }}-amd64
cache-to: type=gha,scope=${{ github.repository }}-amd64,mode=max
cache-from: ${{ runner.environment == 'github-hosted' && format('type=gha,scope={0}-amd64', github.repository) || '' }}
cache-to: ${{ runner.environment == 'github-hosted' && format('type=gha,scope={0}-amd64,mode=max', github.repository) || '' }}
build-args: |
GITHUB_USER=${{ github.actor }}
secrets: |
Expand Down
21 changes: 15 additions & 6 deletions tests/test_docker_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ def resolve(value, context):
return value
def expression(match):
source = match.group(1).strip()
source = re.sub(r'\b(?:github|inputs|steps|env)\.[\w.]+', lambda field: repr(context[field[0]]), source)
source = re.sub(r'\b(?:github|inputs|steps|env|runner)\.[\w.]+', lambda field: repr(context[field[0]]), source)
# GitHub's format('{0}', x); evaluated as Python's str.format.
source = re.sub(r'\bformat\(', 'str.format(', source)
source = source.replace('&&', ' and ').replace('||', ' or ')
return str(eval('(' + source + ')', {'__builtins__': {}}, {}))
return str(eval('(' + source + ')', {'__builtins__': {}, 'str': str}, {}))
return re.sub(r'\$\{\{(.*?)\}\}', expression, value, flags=re.DOTALL)


Expand Down Expand Up @@ -49,6 +51,8 @@ def test_build_and_cache_contract_for_public_and_private_events(self):
'github.event_name': event,
'github.repository': 'groundsgg/plugin-notifications',
'inputs.runner': '',
# Public repos land on GitHub's runners, private ones on their ARC set.
'runner.environment': 'github-hosted' if visibility == 'public' else 'self-hosted',
}
active = [step for step in job['steps'] if resolve('${{ ' + step['if'] + ' }}' if 'if' in step else True, context) not in ['False', False]]
builders = [step for step in active if 'setup-buildx-action' in step.get('uses', '')]
Expand All @@ -60,17 +64,22 @@ def test_build_and_cache_contract_for_public_and_private_events(self):
options = step['with']
self.assertEqual(options['platforms'], 'linux/amd64')
self.assertIn('github_token=', options['secrets'])
self.assertEqual(resolve(options['cache-from'], context), 'type=gha,scope=groundsgg/plugin-notifications-amd64')
# The GHA layer cache downloads at tens of KB/s on ARC; hosted runners only.
expected_from = 'type=gha,scope=groundsgg/plugin-notifications-amd64' if visibility == 'public' else ''
self.assertEqual(resolve(options['cache-from'], context), expected_from)
pushes = [step for step in builds if step['with'].get('push') is True]
cache_writes = [step for step in builds if step['with'].get('cache-to')]
cache_writes = [step for step in builds if resolve(step['with'].get('cache-to', ''), context)]
if event == 'pull_request':
self.assertEqual(pushes, [])
self.assertEqual(cache_writes, [])
self.assertTrue(any(step['with'].get('load') for step in builds))
else:
self.assertEqual(len(pushes), 1)
self.assertEqual(len(cache_writes), 1)
self.assertIn('mode=max', cache_writes[0]['with']['cache-to'])
if visibility == 'public':
self.assertEqual(len(cache_writes), 1)
self.assertIn('mode=max', resolve(cache_writes[0]['with']['cache-to'], context))
else:
self.assertEqual(cache_writes, [], 'ARC builds must not export to the GHA cache')
expected_runner = 'ubuntu-24.04' if visibility == 'public' else 'plugin-notifications'
self.assertEqual(resolve(job['runs-on'], context).strip(), expected_runner)
expected_budget = 30 if visibility == 'public' else 60
Expand Down