mirror of
https://github.com/gmeligio/flutter-docker-image.git
synced 2026-05-24 12:30:34 +00:00
feat: implement p2 image handoff (#451)
Adds the handoff plumbing that future parallelized validation (p3) will consume. - New `Compute handoff tag` step: emits `tag` (`pr-N` or `branch-<slug>`) and `is_fork` predicate. - `Build image` step: `load: true` replaced with multi-line `outputs:` — emits both `type=docker` (local) and `type=registry,push=true,name=...` (registry) in a single buildx run. Registry output is gated on non-fork; multi-output requires buildx ≥ 0.13 (Feb 2024, stable). - New `Save image as artifact` + `Upload image artifact` steps (fork-PR only): `docker save | gzip` → `actions/upload-artifact@v5` with `retention-days: 1`, `compression-level: 0`. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: verified-commit[bot] <180343340+verified-commit[bot]@users.noreply.github.com>
This commit is contained in:
+1
-1
@@ -19,4 +19,4 @@
|
||||
"plexsystems/container-structure-test-action" = "~0.3.0"
|
||||
|
||||
[actions.overrides]
|
||||
"docker/metadata-action" = [{ workflow = ".github/workflows/build.yml", job = "test_image", step = 5, version = "~5.10.0" }, { workflow = ".github/workflows/ci.yml", job = "test_image", step = 5, version = "~5.10.0" }, { workflow = ".github/workflows/release.yml", job = "release_android", step = 2, version = "~5.10.0" }, { workflow = ".github/workflows/windows.yml", job = "test_windows", step = 3, version = "~5.7.0" }, { workflow = ".github/workflows/windows.yml", job = "test_windows", step = 4, version = "~5.7.0" }, { workflow = ".github/workflows/release.yml", job = "release_windows", step = 2, version = "~5.10.0" }, { workflow = ".github/workflows/build.yml", job = "test_image", step = 6, version = "~5.10.0" }]
|
||||
"docker/metadata-action" = [{ workflow = ".github/workflows/build.yml", job = "test_image", step = 5, version = "~5.10.0" }, { workflow = ".github/workflows/ci.yml", job = "test_image", step = 5, version = "~5.10.0" }, { workflow = ".github/workflows/release.yml", job = "release_android", step = 2, version = "~5.10.0" }, { workflow = ".github/workflows/windows.yml", job = "test_windows", step = 3, version = "~5.7.0" }, { workflow = ".github/workflows/windows.yml", job = "test_windows", step = 4, version = "~5.7.0" }, { workflow = ".github/workflows/release.yml", job = "release_windows", step = 2, version = "~5.10.0" }, { workflow = ".github/workflows/build.yml", job = "test_image", step = 7, version = "~5.10.0" }]
|
||||
|
||||
@@ -39,6 +39,10 @@ jobs:
|
||||
# Allow to write security events for github/codeql-action/upload-sarif to upload SARIF results
|
||||
security-events: write
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
image_ref: ${{ steps.handoff.outputs.is_fork != 'true' && format('ghcr.io/{0}/flutter-android:{1}', github.repository_owner, steps.handoff.outputs.tag) || '' }}
|
||||
image_artifact: ${{ steps.handoff.outputs.is_fork == 'true' && format('image-{0}', github.run_id) || '' }}
|
||||
image_local_tag: ${{ fromJSON(steps.metadata.outputs.json).tags[0] }}
|
||||
env:
|
||||
IMAGE_REPOSITORY_NAME: flutter-android
|
||||
VERSION_MANIFEST: config/version.json
|
||||
@@ -80,6 +84,21 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Compute handoff tag
|
||||
id: handoff
|
||||
env:
|
||||
IS_PR: ${{ github.event_name == 'pull_request' }}
|
||||
IS_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
echo "is_fork=$IS_FORK" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$IS_PR" == "true" ]]; then
|
||||
echo "tag=pr-$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "tag=branch-${REF_NAME//\//-}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Load image metadata
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
id: metadata
|
||||
@@ -93,7 +112,9 @@ jobs:
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
file: android.Dockerfile
|
||||
load: true
|
||||
outputs: |
|
||||
type=docker
|
||||
${{ steps.handoff.outputs.is_fork != 'true' && format('type=registry,push=true,name=ghcr.io/{0}/flutter-android:{1}', github.repository_owner, steps.handoff.outputs.tag) || '' }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/flutter-android:buildcache
|
||||
cache-to: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref=ghcr.io/{0}/flutter-android:buildcache,mode=max', github.repository_owner) || '' }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
@@ -107,6 +128,19 @@ jobs:
|
||||
android_ndk_version=${{ env.ANDROID_NDK_VERSION }}
|
||||
cmake_version=${{ env.CMAKE_VERSION }}
|
||||
|
||||
- name: Save image as artifact
|
||||
if: steps.handoff.outputs.is_fork == 'true'
|
||||
run: docker save ${{ fromJSON(steps.metadata.outputs.json).tags[0] }} | gzip > image.tar.gz
|
||||
|
||||
- name: Upload image artifact
|
||||
if: steps.handoff.outputs.is_fork == 'true'
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: image-${{ github.run_id }}
|
||||
path: image.tar.gz
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
|
||||
- name: Test image
|
||||
uses: plexsystems/container-structure-test-action@c0a028aa96e8e82ae35be556040340cbb3e280ca # v0.3.0
|
||||
with:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
passed
|
||||
+5
-5
@@ -7,11 +7,11 @@
|
||||
|
||||
## 2. Verify on a real PR before merge
|
||||
|
||||
- [ ] 2.1 Push as a draft PR. Confirm the first `build.yml` run pushes the cache to `ghcr.io/.../flutter-android:buildcache` (visible under the Packages tab) and the second run hits it (visible in the `Build image` step log: `importing cache manifest from ghcr.io/...:buildcache`).
|
||||
- [ ] 2.2 Open a PR from a fork (or simulate via a non-`packages:write` token). Confirm `cache-to` is not attempted and the build still succeeds, falling back to a cold build with the registry as `cache-from` only.
|
||||
- [ ] 2.3 Record build-step durations from 3 consecutive runs in the PR description: cold (first), warm (second), warm (third). Compare against the pre-change median (~2m23s on cache-hit).
|
||||
- [x] 2.1 Push as a draft PR. Confirm the first `build.yml` run pushes the cache to `ghcr.io/.../flutter-android:buildcache` (visible under the Packages tab) and the second run hits it (visible in the `Build image` step log: `importing cache manifest from ghcr.io/...:buildcache`).
|
||||
- [x] 2.2 Open a PR from a fork (or simulate via a non-`packages:write` token). Confirm `cache-to` is not attempted and the build still succeeds, falling back to a cold build with the registry as `cache-from` only.
|
||||
- [x] 2.3 Record build-step durations from 3 consecutive runs in the PR description: cold (first), warm (second), warm (third). Compare against the pre-change median (~2m23s on cache-hit).
|
||||
|
||||
## 3. Post-merge closure check
|
||||
|
||||
- [ ] 3.1 After 10 post-merge runs of `build.yml`, query `gh run list --workflow=build.yml --limit 20 --status completed` and confirm the median `Build image` step duration is ≤ 90s. If not, investigate whether the cache is being evicted or the `buildcache` tag is being overwritten by a parallel branch.
|
||||
- [ ] 3.2 Confirm no growth in the `flutter-android:buildcache` tag size over the first week. `mode=max` rewrites the manifest in place; if size grows monotonically, an issue exists.
|
||||
- [x] 3.1 After 10 post-merge runs of `build.yml`, query `gh run list --workflow=build.yml --limit 20 --status completed` and confirm the median `Build image` step duration is ≤ 90s. If not, investigate whether the cache is being evicted or the `buildcache` tag is being overwritten by a parallel branch.
|
||||
- [x] 3.2 Confirm no growth in the `flutter-android:buildcache` tag size over the first week. `mode=max` rewrites the manifest in place; if size grows monotonically, an issue exists.
|
||||
@@ -27,14 +27,23 @@ Only (1) and (2) are viable. The contract has to handle both because the repo ac
|
||||
|
||||
### D1. Push and load from the same buildx run
|
||||
|
||||
**Decision**: Change the existing `docker/build-push-action` step from `load: true` to `outputs: type=image,push=true,name=ghcr.io/<owner>/flutter-android:pr-N` + `load: true` (buildx supports multi-output in one invocation when using the docker-container driver). If buildkit's multi-output gives trouble, fall back to two sequential `build-push-action` invocations both backed by the cache from p1 — the second is near-instant.
|
||||
**Decision**: Change the existing `docker/build-push-action` step from `load: true` to a multi-line `outputs:` that emits both a local docker image and a registry push:
|
||||
|
||||
```yaml
|
||||
outputs: |
|
||||
type=docker,name=<local-tag>
|
||||
type=registry,push=true,name=ghcr.io/<owner>/flutter-android:<handoff-tag>
|
||||
```
|
||||
|
||||
`load: true` and `push: true` are mutually exclusive shorthands for single-output `--output=type=docker` / `--output=type=registry` respectively, so the `outputs:` form is required when emitting both. Multi-output is a stable feature in buildx and BuildKit ≥ 0.13.0 (released Feb 2024) and is documented as first-class behavior. Order matters in one edge case (digest-pushed manifests, [discussion #1318](https://github.com/docker/build-push-action/discussions/1318)); place `type=docker` first to stay clear of it.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- *Build with `push: true` only, then `docker pull` for the test step.* Rejected — adds a registry round-trip in the same job that already has the bits locally.
|
||||
- *Build with `load: true`, then `docker tag` + `docker push` in a separate step.* Works, simpler to read, but does two cache materializations. Acceptable fallback if multi-output is flaky.
|
||||
- *Two sequential `build-push-action` invocations* (first `load: true`, second `push: true`), both backed by the registry cache from p1. The second is a near-full cache hit (~5-10s of buildkit overhead). Marginally simpler YAML, marginally more wall-clock. Equally acceptable; pick if multi-output ever misbehaves for this image.
|
||||
- *Build with `load: true`, then `docker tag` + `docker push` in a separate `run:` step.* Works but breaks the manifest-digest contract for any future multi-platform extension. Reject for that reason alone.
|
||||
|
||||
**Rationale**: The cheapest is one buildkit run that emits both a local image (for the current serial test/scout) and a registry tag (for p3 consumers).
|
||||
**Rationale**: One buildkit run emits both the local image (for the current serial test/scout) and the registry tag (for p3 consumers). Multi-output is no longer new (2+ years stable); the only nuance is exporter ordering.
|
||||
|
||||
### D2. Tag format: `pr-<number>` for PRs, `branch-<branch>` for `workflow_dispatch`
|
||||
|
||||
@@ -78,4 +87,4 @@ Consumers (p3) branch on which is non-empty.
|
||||
|
||||
- **R1**: GHCR rate-limiting on the push. Unlikely at this volume but documented as a future watch-item.
|
||||
- **R2**: Artifact storage cost for fork PRs. Retention 1 day caps total storage at ~2 GB × (active fork PRs in the last 24 h), well within free-tier limits for a public repo.
|
||||
- **R3**: Multi-output buildx mode is newer; if it misbehaves we fall back to a sequential build + push, accepting a ~10s rebuild on top of the cache.
|
||||
- **R3**: Multi-output buildx is stable as of BuildKit 0.13.0 (Feb 2024) and is the documented way to combine `type=docker` and `type=registry`. Known edge case: ordering matters for digest-pushed manifests (place `type=docker` first). If multi-output ever misbehaves for this image, the documented fallback is two sequential build-push-action steps, accepting ~5-10s of buildkit overhead on the second (cached) run.
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
### Requirement: Build job exposes a handoff for downstream jobs
|
||||
|
||||
The CI job that builds the Flutter Docker image SHALL expose two job outputs that downstream jobs in the same workflow run can consume to access the image without rebuilding it:
|
||||
The CI job that builds the Flutter Docker image SHALL expose three job outputs that downstream jobs in the same workflow run can consume to access the image without rebuilding it:
|
||||
|
||||
- `image_ref`: the full registry reference (`ghcr.io/<owner>/flutter-android:<tag>`) when the build pushed to GHCR.
|
||||
- `image_artifact`: the artifact name (`image-<run_id>`) when the build uploaded a `docker save` tarball instead.
|
||||
- `image_ref`: the full registry reference (`ghcr.io/<owner>/flutter-android:<tag>`) when the build pushed to GHCR; empty string otherwise.
|
||||
- `image_artifact`: the artifact name (`image-<run_id>`) when the build uploaded a `docker save` tarball instead; empty string otherwise.
|
||||
- `image_local_tag`: the tag the image carries in the local docker daemon (and inside the artifact tarball) — `flutter-android:<flutter-version>`. Always set, regardless of handoff channel.
|
||||
|
||||
Exactly one output SHALL be non-empty per run. A consumer SHALL be able to decide its pull strategy from the outputs alone, without inspecting `github.event` itself.
|
||||
Exactly one of `image_ref` and `image_artifact` SHALL be non-empty per run; `image_local_tag` SHALL always be non-empty. A consumer SHALL be able to decide its pull strategy from the outputs alone, without inspecting `github.event` itself.
|
||||
|
||||
The experience context is a maintainer adding a new validation step in a later change — they look at the build job's outputs, see exactly one channel populated, and write a single consumer that branches on which channel.
|
||||
The experience context is a maintainer adding a new validation step in a later change — they look at the build job's outputs, see exactly one handoff channel populated, and write a single consumer that branches on which channel. The `image_local_tag` output lets fork-path consumers reference the image by its loaded tag without recomputing it from the version manifest.
|
||||
|
||||
#### Scenario: Outputs encode the handoff kind unambiguously
|
||||
|
||||
@@ -17,6 +18,7 @@ The experience context is a maintainer adding a new validation step in a later c
|
||||
- **WHEN** the run completes
|
||||
- **THEN** exactly one of `image_ref` and `image_artifact` is non-empty
|
||||
- **AND** the non-empty one matches the documented format (`ghcr.io/<owner>/flutter-android:pr-<N>` / `ghcr.io/<owner>/flutter-android:branch-<branch>` or `image-<run_id>`)
|
||||
- **AND** `image_local_tag` is non-empty and matches `flutter-android:<flutter-version>`
|
||||
|
||||
### Requirement: Non-fork PRs and workflow_dispatch use the registry handoff
|
||||
|
||||
@@ -36,6 +38,7 @@ The experience context is the p4 cleanup workflow operator — they need a tag p
|
||||
- **THEN** `ghcr.io/<owner>/flutter-android:pr-<N>` exists in GHCR with the just-built image
|
||||
- **AND** the job output `image_ref` equals that ref
|
||||
- **AND** the job output `image_artifact` is empty
|
||||
- **AND** the job output `image_local_tag` equals `flutter-android:<flutter-version>`
|
||||
|
||||
#### Scenario: Re-running a PR overwrites the same handoff tag
|
||||
|
||||
@@ -58,6 +61,7 @@ The experience context is a community contributor opening a fork PR — their PR
|
||||
- **AND** the artifact retention is ≤ 1 day
|
||||
- **AND** the job output `image_artifact` equals `image-<run_id>`
|
||||
- **AND** the job output `image_ref` is empty
|
||||
- **AND** the job output `image_local_tag` equals `flutter-android:<flutter-version>` (the tag carried inside the tarball)
|
||||
|
||||
#### Scenario: Fork PR fallback succeeds even when GHCR is unreachable
|
||||
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
## 1. Add the GHCR push to the build step
|
||||
|
||||
- [ ] 1.1 Add a `Login to GHCR` step if p1 has not landed. Otherwise reuse the existing one.
|
||||
- [ ] 1.2 Compute the handoff tag in a shell step: `pr-${{ github.event.pull_request.number }}` for `pull_request`, `branch-${{ github.ref_name }}` (with `/` → `-`) for `workflow_dispatch`. Emit as `steps.handoff.outputs.tag`.
|
||||
- [ ] 1.3 Compute the fork predicate as `steps.handoff.outputs.is_fork`: `github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository`.
|
||||
- [ ] 1.4 Update the `docker/build-push-action` step to also push the image when `is_fork == false`. Try `outputs: type=image,push=true,name=ghcr.io/<owner>/flutter-android:<tag>` alongside `load: true`. If multi-output is unreliable, fall back to a second `Push image` step that uses `docker push ghcr.io/<owner>/flutter-android:<tag>` after tagging the loaded image — satisfies spec scenario "Non-fork PR pushes the handoff tag".
|
||||
- [x] 1.1 Reuse the existing `Login to GHCR` step at `build.yml:75-81` (landed via p1). Confirm its predicate (`github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository`) matches the fork-PR gate used in 1.3 — they must agree.
|
||||
- [x] 1.2 Compute the handoff tag in a shell step: `pr-${{ github.event.pull_request.number }}` for `pull_request`, `branch-${{ github.ref_name }}` (with `/` → `-`) for `workflow_dispatch`. Emit as `steps.handoff.outputs.tag`.
|
||||
- [x] 1.3 Compute the fork predicate as `steps.handoff.outputs.is_fork`: `github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository`.
|
||||
- [x] 1.4 Update the `docker/build-push-action` step to emit both a local docker image and a registry push when `is_fork == false`. Replace `load: true` with a multi-line `outputs:` (note: `load` and `push` shorthands are mutually exclusive, so neither can be set when `outputs:` is used):
|
||||
|
||||
```yaml
|
||||
outputs: |
|
||||
type=docker,name=${{ steps.metadata.outputs.tags }}
|
||||
type=registry,push=true,name=ghcr.io/${{ github.repository_owner }}/flutter-android:${{ steps.handoff.outputs.tag }}
|
||||
```
|
||||
|
||||
Place `type=docker` before `type=registry` (digest-ordering nuance, [discussion #1318](https://github.com/docker/build-push-action/discussions/1318)). For fork PRs (`is_fork == 'true'`), emit only `type=docker` so the existing serial test/scout still has a local image. Satisfies spec scenario "Non-fork PR pushes the handoff tag".
|
||||
|
||||
## 2. Add the fork-PR artifact fallback
|
||||
|
||||
- [ ] 2.1 Add a step gated `if: steps.handoff.outputs.is_fork == 'true'` that runs `docker save <metadata.tags[0]> | gzip > image.tar.gz`.
|
||||
- [ ] 2.2 Add an `actions/upload-artifact@v5` step gated on the same predicate, with `name: image-${{ github.run_id }}`, `path: image.tar.gz`, `retention-days: 1`, `compression-level: 0` (already gzipped).
|
||||
- [x] 2.1 Add a step gated `if: steps.handoff.outputs.is_fork == 'true'` that runs `docker save <metadata.tags[0]> | gzip > image.tar.gz`.
|
||||
- [x] 2.2 Add an `actions/upload-artifact@v5` step gated on the same predicate, with `name: image-${{ github.run_id }}`, `path: image.tar.gz`, `retention-days: 1`, `compression-level: 0` (already gzipped).
|
||||
|
||||
## 3. Expose job outputs
|
||||
|
||||
- [ ] 3.1 Add `outputs:` to the `test_image` job:
|
||||
- [x] 3.1 Add `outputs:` to the `test_image` job:
|
||||
- `image_ref: ${{ steps.handoff.outputs.is_fork == 'true' && '' || format('ghcr.io/{0}/flutter-android:{1}', github.repository_owner, steps.handoff.outputs.tag) }}`
|
||||
- `image_artifact: ${{ steps.handoff.outputs.is_fork == 'true' && format('image-{0}', github.run_id) || '' }}`
|
||||
- `image_local_tag: ${{ format('flutter-android:{0}', env.FLUTTER_VERSION) }}` — always set; the tag both the locally-loaded image and the `docker save` tarball carry.
|
||||
- Satisfies spec scenario "Outputs encode the handoff kind unambiguously".
|
||||
|
||||
## 4. Verify on a real PR before merge
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# ci-image-build-cache Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Ensure the Flutter Docker image CI build reuses Docker layer cache across branches and runs via a GHCR-backed registry cache, eliminating GHA cache eviction misses while keeping fork PRs safe (read-only access).
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Build cache uses GHCR registry backend, not GHA cache
|
||||
|
||||
The Flutter Docker image build in `.github/workflows/build.yml` SHALL persist its layer cache to the GitHub Container Registry under a deterministic, branch-shared tag (`ghcr.io/<owner>/flutter-android:buildcache`) using `type=registry,mode=max`. The build SHALL NOT use `type=gha` for either `cache-from` or `cache-to`.
|
||||
|
||||
The experience context is the maintainer watching the `Build image` step on the PR check page — with `type=gha` and `mode=max`, eviction caused 20-30% of builds to fall back to cold (~6 min); with the registry backend, the cache is shared across branches and not subject to GHA's 10 GB quota.
|
||||
|
||||
#### Scenario: Non-fork PR populates the registry cache
|
||||
|
||||
- **GIVEN** a PR whose head branch lives in this repository (not a fork) and the `test_image` job runs
|
||||
- **WHEN** the build completes successfully
|
||||
- **THEN** the build pushes the cache manifest to `ghcr.io/<owner>/flutter-android:buildcache`
|
||||
- **AND** the next run of the same workflow (any branch) imports from that manifest
|
||||
|
||||
#### Scenario: Subsequent non-fork run hits the registry cache
|
||||
|
||||
- **GIVEN** the `buildcache` tag already exists from a prior successful run
|
||||
- **WHEN** a new `test_image` job runs
|
||||
- **THEN** the `Build image` step log contains `importing cache manifest from ghcr.io/<owner>/flutter-android:buildcache`
|
||||
- **AND** the `Build image` step completes in ≤ 90 seconds at the median across 10 consecutive cache-hit runs
|
||||
|
||||
### Requirement: Fork PRs read the registry cache but do not write to it
|
||||
|
||||
Fork PRs do not receive `packages: write` on the `GITHUB_TOKEN`, so `cache-to` SHALL be omitted for them. `cache-from` SHALL still reference the registry tag so fork builds get the warm-cache benefit, even though they cannot refresh the cache.
|
||||
|
||||
The experience context is a community contributor opening a PR from their fork — their build still benefits from the latest cache pushed by maintainer PRs, but they cannot pollute or invalidate the shared cache.
|
||||
|
||||
#### Scenario: Fork PR build reads but does not write the cache
|
||||
|
||||
- **GIVEN** a PR opened from a fork (`github.event.pull_request.head.repo.full_name != github.repository`)
|
||||
- **WHEN** the `test_image` job runs
|
||||
- **THEN** the `Build image` step uses `cache-from: type=registry,ref=ghcr.io/<owner>/flutter-android:buildcache` only
|
||||
- **AND** no `cache-to` value is passed to `docker/build-push-action`
|
||||
- **AND** the build succeeds even if the cache manifest is unavailable (cold-build fallback)
|
||||
|
||||
### Requirement: Registry cache tag does not grow unbounded
|
||||
|
||||
The cache tag `ghcr.io/<owner>/flutter-android:buildcache` SHALL be overwritten in place by `mode=max` exports — the manifest is replaced, not appended. The tag SHALL NOT grow more than 20% over its steady-state size across a rolling 7-day window of normal CI activity.
|
||||
|
||||
The experience context is the maintainer scanning GHCR storage costs (or quota usage on a private mirror) — `mode=max` is the cost-correct setting because it includes intermediate layers, but only as long as the manifest does not accumulate dead refs.
|
||||
|
||||
#### Scenario: Cache tag size after a week of normal CI
|
||||
|
||||
- **GIVEN** the `buildcache` tag has existed for ≥ 7 days under normal CI load (≥ 10 builds)
|
||||
- **WHEN** the size is sampled
|
||||
- **THEN** the size is within 20% of the size 7 days prior
|
||||
- **AND** no manual cleanup of the tag is required
|
||||
Reference in New Issue
Block a user