mirror of
https://github.com/gmeligio/flutter-docker-image.git
synced 2026-05-24 12:30:34 +00:00
ci: parallelize image validation (#453)
- Splits the monolithic `test_image` job (~20m serial) into `build_image` + parallel `test_image` (CST) and `scan_image` (Scout) consumers — wall-clock drops to ~15m (~5m saved per PR). --------- 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:
@@ -4,9 +4,15 @@ version = "v6.0.1"
|
||||
[resolutions."actions/create-github-app-token"."^2"]
|
||||
version = "v2.2.1"
|
||||
|
||||
[resolutions."actions/download-artifact"."^4"]
|
||||
version = "v4"
|
||||
|
||||
[resolutions."actions/download-artifact"."^6"]
|
||||
version = "v6.0.0"
|
||||
|
||||
[resolutions."actions/download-artifact"."~6.0.0"]
|
||||
version = "v6.0.0"
|
||||
|
||||
[resolutions."actions/github-script"."^8"]
|
||||
version = "v8.0.0"
|
||||
|
||||
@@ -34,6 +40,12 @@ version = "v5.7.0"
|
||||
[resolutions."docker/scout-action"."^1"]
|
||||
version = "v1.18.2"
|
||||
|
||||
[resolutions."docker/scout-action"."~1.18.2"]
|
||||
version = "v1.18.2"
|
||||
|
||||
[resolutions."docker/scout-action"."~1.20.4"]
|
||||
version = "v1.20.4"
|
||||
|
||||
[resolutions."docker/setup-buildx-action"."^3"]
|
||||
version = "v3.11.1"
|
||||
|
||||
@@ -70,6 +82,12 @@ repository = "actions/create-github-app-token"
|
||||
ref_type = "tag"
|
||||
date = "2025-12-05T22:53:03Z"
|
||||
|
||||
[actions."actions/download-artifact".v4]
|
||||
sha = "d3f86a106a0bac45b974a628896c90dbdf5c8093"
|
||||
repository = "actions/download-artifact"
|
||||
ref_type = "tag"
|
||||
date = "2025-04-24T16:25:03Z"
|
||||
|
||||
[actions."actions/download-artifact"."v6.0.0"]
|
||||
sha = "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53"
|
||||
repository = "actions/download-artifact"
|
||||
@@ -124,6 +142,12 @@ repository = "docker/scout-action"
|
||||
ref_type = "tag"
|
||||
date = "2025-07-21T12:52:07Z"
|
||||
|
||||
[actions."docker/scout-action"."v1.20.4"]
|
||||
sha = "bacf462e8d090c09660de30a6ccc718035f961e3"
|
||||
repository = "docker/scout-action"
|
||||
ref_type = "release"
|
||||
date = "2026-04-08T08:04:55Z"
|
||||
|
||||
[actions."docker/setup-buildx-action"."v3.11.1"]
|
||||
sha = "e468171a9de216ec08956ac3ada2f0791b6bd435"
|
||||
repository = "docker/setup-buildx-action"
|
||||
|
||||
+3
-1
@@ -19,4 +19,6 @@
|
||||
"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 = 7, version = "~5.10.0" }]
|
||||
"actions/download-artifact" = [{ workflow = ".github/workflows/build.yml", job = "test_image", step = 2, version = "^4" }, { workflow = ".github/workflows/build.yml", job = "scan_image", step = 0, version = "^4" }, { workflow = ".github/workflows/update_version.yml", job = "validate_config_version", step = 2, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_docs_and_create_pr", step = 2, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_docs_and_create_pr", step = 3, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_android_version", step = 2, version = "~6.0.0" }]
|
||||
"docker/metadata-action" = [ { 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 = "build_image", step = 7, version = "~5.10.0" }]
|
||||
"docker/scout-action" = [{ workflow = ".github/workflows/build.yml", job = "scan_image", step = 3, version = "~1.20.4" }, { workflow = ".github/workflows/release.yml", job = "record_image", step = 2, version = "~1.18.2" }, { workflow = ".github/workflows/build.yml", job = "scan_image", step = 5, version = "~1.20.4" }]
|
||||
|
||||
+121
-24
@@ -30,19 +30,21 @@ jobs:
|
||||
version=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.tag_name')
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
test_image:
|
||||
build_image:
|
||||
permissions:
|
||||
# Allow to write packages for GHCR cache push and docker/scout-action
|
||||
packages: write
|
||||
# Allow to write pull requests for the docker/scout-action to write a comment
|
||||
pull-requests: write
|
||||
# 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 is set per the p2 contract but GitHub Actions suppresses
|
||||
# any job output whose value contains a registered secret. When
|
||||
# github.repository_owner equals DOCKER_HUB_USERNAME (as on this repo),
|
||||
# image_ref is masked-and-dropped. Consumers MUST use image_tag and
|
||||
# reconstruct ghcr.io/${{ github.repository_owner }}/flutter-android:<tag>.
|
||||
image_ref: ${{ steps.handoff.outputs.is_fork != 'true' && format('ghcr.io/{0}/flutter-android:{1}', github.repository_owner, steps.handoff.outputs.tag) || '' }}
|
||||
image_tag: ${{ steps.handoff.outputs.is_fork != 'true' && 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] }}
|
||||
image_local_tag: ${{ steps.local_tag.outputs.ref }}
|
||||
flutter_version: ${{ env.FLUTTER_VERSION }}
|
||||
env:
|
||||
IMAGE_REPOSITORY_NAME: flutter-android
|
||||
VERSION_MANIFEST: config/version.json
|
||||
@@ -108,13 +110,15 @@ jobs:
|
||||
tags: |
|
||||
type=raw,value=${{ env.FLUTTER_VERSION }}
|
||||
|
||||
# outputs is just `type=docker` (load into local daemon). The GHCR
|
||||
# handoff push is done by an explicit `docker push` step below — when
|
||||
# combined with `type=docker`, buildkit silently ignores a `type=registry`
|
||||
# output and pushes only via --tag (which points at docker.io, not GHCR).
|
||||
- name: Build image and push to local Docker daemon
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
file: android.Dockerfile
|
||||
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) || '' }}
|
||||
outputs: type=docker
|
||||
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 }}
|
||||
@@ -128,9 +132,28 @@ jobs:
|
||||
android_ndk_version=${{ env.ANDROID_NDK_VERSION }}
|
||||
cmake_version=${{ env.CMAKE_VERSION }}
|
||||
|
||||
# Re-tag the loaded image to a name that does NOT contain the owner.
|
||||
# `image_local_tag` is exposed as a job output; GitHub Actions drops
|
||||
# outputs whose value contains a registered secret (DOCKER_HUB_USERNAME
|
||||
# == github.repository_owner on this repo), so the metadata tag
|
||||
# `<owner>/flutter-android:<version>` cannot be passed through.
|
||||
- name: Re-tag image for local handoff
|
||||
id: local_tag
|
||||
run: |
|
||||
DEST="flutter-android:${{ env.FLUTTER_VERSION }}"
|
||||
docker tag "${{ fromJSON(steps.metadata.outputs.json).tags[0] }}" "$DEST"
|
||||
echo "ref=$DEST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Push image to GHCR
|
||||
if: steps.handoff.outputs.is_fork != 'true'
|
||||
run: |
|
||||
GHCR_REF="ghcr.io/${{ github.repository_owner }}/flutter-android:${{ steps.handoff.outputs.tag }}"
|
||||
docker tag "${{ fromJSON(steps.metadata.outputs.json).tags[0] }}" "$GHCR_REF"
|
||||
docker push "$GHCR_REF"
|
||||
|
||||
- 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
|
||||
run: docker save "${{ steps.local_tag.outputs.ref }}" | gzip > image.tar.gz
|
||||
|
||||
- name: Upload image artifact
|
||||
if: steps.handoff.outputs.is_fork == 'true'
|
||||
@@ -141,32 +164,106 @@ jobs:
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
|
||||
test_image:
|
||||
needs: build_image
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Clean runner disk
|
||||
if: needs.build_image.outputs.image_artifact != ''
|
||||
uses: ./.github/actions/clean-runner-disk
|
||||
|
||||
- name: Download image artifact
|
||||
if: needs.build_image.outputs.image_artifact != ''
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: ${{ needs.build_image.outputs.image_artifact }}
|
||||
|
||||
- name: Load image from artifact
|
||||
if: needs.build_image.outputs.image_artifact != ''
|
||||
run: gunzip -c image.tar.gz | docker load
|
||||
|
||||
- name: Login to GHCR
|
||||
if: needs.build_image.outputs.image_artifact == ''
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull image from registry
|
||||
if: needs.build_image.outputs.image_artifact == ''
|
||||
run: docker pull "ghcr.io/${{ github.repository_owner }}/flutter-android:${{ needs.build_image.outputs.image_tag }}"
|
||||
|
||||
- name: Test image
|
||||
uses: plexsystems/container-structure-test-action@c0a028aa96e8e82ae35be556040340cbb3e280ca # v0.3.0
|
||||
with:
|
||||
image: ${{ fromJSON(steps.metadata.outputs.json).tags[0] }}
|
||||
image: ${{ needs.build_image.outputs.image_artifact != '' && needs.build_image.outputs.image_local_tag || format('ghcr.io/{0}/flutter-android:{1}', github.repository_owner, needs.build_image.outputs.image_tag) }}
|
||||
config: test/android.yml
|
||||
|
||||
# TODO: Parallelize testing and vulnerability scanning
|
||||
# Scout needs the Docker Hub org secret and writes a PR comment, neither
|
||||
# of which is available to fork PRs.
|
||||
scan_image:
|
||||
needs: build_image
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
permissions:
|
||||
packages: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Download image artifact
|
||||
if: needs.build_image.outputs.image_artifact != ''
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: ${{ needs.build_image.outputs.image_artifact }}
|
||||
|
||||
- name: Load image from artifact
|
||||
if: needs.build_image.outputs.image_artifact != ''
|
||||
run: gunzip -c image.tar.gz | docker load
|
||||
|
||||
# Docker Hub login is required by Scout — it authenticates against the
|
||||
# Docker Hub identity tied to the org secret, not the GHCR identity.
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
if: needs.build_image.outputs.image_artifact == ''
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Pull the PR-tagged GHCR image and re-tag it with the Docker Hub repo
|
||||
# identity (<owner>/flutter-android:<version>). Scout's `compare` looks
|
||||
# up the image's repo in its environment records — those records exist
|
||||
# for the Docker Hub repo, not for `ghcr.io/<owner>/flutter-android`.
|
||||
# Without this re-tag, Scout fails with "not in stream environment:prod".
|
||||
- name: Pull image and re-tag for Scout
|
||||
if: needs.build_image.outputs.image_artifact == ''
|
||||
run: |
|
||||
GHCR_REF="ghcr.io/${{ github.repository_owner }}/flutter-android:${{ needs.build_image.outputs.image_tag }}"
|
||||
SCOUT_REF="${{ github.repository_owner }}/flutter-android:${{ needs.build_image.outputs.flutter_version }}"
|
||||
docker pull "$GHCR_REF"
|
||||
docker tag "$GHCR_REF" "$SCOUT_REF"
|
||||
|
||||
- name: Scan with Docker Scout
|
||||
id: docker-scout
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
uses: docker/scout-action@f8c776824083494ab0d56b8105ba2ca85c86e4de # v1.18.2
|
||||
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
|
||||
with:
|
||||
command: compare, recommendations
|
||||
# Use the Docker Hub image that is the first tag in the metadata
|
||||
image: local://${{ fromJson(steps.metadata.outputs.json).tags[0] }}
|
||||
# github-token is needed to be able to write the PR comment
|
||||
image: local://${{ github.repository_owner }}/flutter-android:${{ needs.build_image.outputs.flutter_version }}
|
||||
github-token: ${{ github.token }}
|
||||
only-fixed: true
|
||||
organization: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
# sarif-file: output.sarif.json
|
||||
to-env: prod
|
||||
# Enable debug logging when needed
|
||||
# debug: true
|
||||
# verbose-debug: true
|
||||
|
||||
validate_version_files:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
@@ -20,19 +20,20 @@ The non-trivial questions are: (a) how do consumer jobs get the bits onto their
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Consumer jobs use the registry pull, not `docker pull` + `docker run`
|
||||
### D1. Consumer jobs reach the image with the cheapest call each tool supports
|
||||
|
||||
**Decision**: For non-fork PRs, consumer jobs reference the image by its registry ref directly:
|
||||
**Decision**: The two consumers use different strategies because the two actions support different things:
|
||||
|
||||
- `container-structure-test`: pass `image: ghcr.io/<owner>/flutter-android:pr-N` (the action pulls under the hood).
|
||||
- `docker/scout-action`: pass `image: registry://ghcr.io/<owner>/flutter-android:pr-N` (the `registry://` prefix tells Scout to skip the local store).
|
||||
- `docker/scout-action`: pass `image: registry://ghcr.io/<owner>/flutter-android:pr-N`. The `registry://` prefix tells Scout to read from the registry directly with no daemon involvement — confirmed in the action's README image-prefix table. No `docker pull` step on the registry path for `scan_image`.
|
||||
- `container-structure-test` (via `plexsystems/container-structure-test-action`): the action invokes `container-structure-test test --image <input>` with no `--pull` flag and no driver override. The CLI's default `docker` driver inspects the local Docker daemon only. So `test_image` SHALL run an explicit `docker pull "$IMAGE_REF"` on the registry path before invoking the action. The earlier draft of this design assumed CST would stream-pull on demand; it does not.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
- *`docker pull` step in each consumer, then run CST/Scout against the local image.* Works but adds ~30s of redundant pull-and-load when the action can stream-pull as needed.
|
||||
- *Replace the plexsystems action with raw `container-structure-test test --pull --image <ref>`.* One step instead of two, and drops a stale dependency (plexsystems' last release was Mar 2023; last commit Aug 2023). Rejected for now: requires a new install step (curl + chmod-and-pin), and the win is ~30 s. Worth revisiting if the action ever blocks an upgrade.
|
||||
- *Swap to CST's `--driver tar` (no daemon at all, e.g. `crane export <ref> | container-structure-test --driver tar -`).* Smallest disk footprint, but the tar driver has historical limitations around `commandTests` that rely on real process execution. Out of scope for p3.
|
||||
- *`docker save`-style artifact even for non-fork PRs.* Rejected — wastes the registry-cache work from p1.
|
||||
|
||||
**Rationale**: Each tool natively supports a registry ref. The fewer hops on the consumer runner, the better.
|
||||
**Rationale**: Match the cheapest path to each action's actual behavior, verified against current upstream sources rather than assumed.
|
||||
|
||||
### D2. Fork-PR consumers `download-artifact` + `docker load`, gated on `image_artifact != ''`
|
||||
|
||||
|
||||
@@ -6,14 +6,15 @@ The existing `build.yml:108` TODO explicitly notes this:
|
||||
|
||||
> `# TODO: Parallelize testing and vulnerability scanning`
|
||||
|
||||
`docker/scout-action` accepts a remote image reference (`registry://` prefix per the action README), and `container-structure-test` runs against any locally-loadable image. With the handoff established by p2, the two consumers can run as sibling jobs that each pull the handoff image independently.
|
||||
`docker/scout-action` accepts a remote image reference (`registry://` prefix per the action README, no daemon required), and `container-structure-test` runs against any image present in the local Docker daemon (the `plexsystems/container-structure-test-action` does not pull on its own, so consumers on the registry path SHALL `docker pull` before invoking it). With the handoff established by p2, the two consumers can run as sibling jobs that each materialize the handoff image independently.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Rename** `test_image` job → `build_image`. Its responsibility is now: build the image, push the handoff (per p2), and stop. Remove the `Test image` and `Scan with Docker Scout` steps from this job.
|
||||
- **New job** `test_image` (`needs: build_image`): pulls the handoff (registry or artifact, depending on `build_image.outputs.image_ref` / `image_artifact`), runs container-structure-test against it.
|
||||
- **New job** `scan_image` (`needs: build_image`): pulls the handoff, runs `docker/scout-action` against it. Gated `if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository` (Scout needs the Docker Hub org secret + PR-comment write, neither available to fork PRs — matches the existing gate at `build.yml:113`).
|
||||
- Both consumer jobs run on `ubuntu-24.04` with a thin checkout + setup-buildx + login + pull-or-load + the existing validation step. No `clean-runner-disk` needed in the consumer jobs because they do not build (they only pull the ~5 GB image).
|
||||
- **New job** `test_image` (`needs: build_image`): materializes the handoff into the local Docker daemon (registry-path: `docker pull <image_ref>`; artifact-path: `download-artifact` + `docker load`), runs container-structure-test against the loaded image.
|
||||
- **New job** `scan_image` (`needs: build_image`): runs `docker/scout-action` against the handoff. Registry-path uses `image: registry://<image_ref>` so no local pull is needed; artifact-path uses `image: local://<image_local_tag>` after `download-artifact` + `docker load`. Gated `if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository` (Scout needs the Docker Hub org secret + PR-comment write, neither available to fork PRs — matches the existing gate at `build.yml:155`).
|
||||
- Both consumer jobs run on `ubuntu-24.04` with a thin checkout + login + pull-or-load + the existing validation step. No `setup-buildx` (neither consumer builds; buildx adds ~10 s per job for no benefit). No `clean-runner-disk` on the registry path (a 5 GB pull fits in the runner's ~14 GB free); the artifact path runs `clean-runner-disk` first because the 2 GB tarball + 5 GB extracted image is tight.
|
||||
- Pin `docker/scout-action` to **v1.20.4** (current as of Apr 2026; today's pin is v1.18.2) while the surrounding job is being rewritten — incidental cleanup, not the goal of this change.
|
||||
- `build_image` keeps `clean-runner-disk` (the build still needs the headroom).
|
||||
- Move the `permissions.security-events: write` from `build_image` to `scan_image` (Scout writes SARIF), and `permissions.pull-requests: write` likewise.
|
||||
|
||||
@@ -28,11 +29,11 @@ The existing `build.yml:108` TODO explicitly notes this:
|
||||
│ Scout ........ 9m │ │
|
||||
└──────────────────────────┘ ┌────┴─────┐
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────┐
|
||||
│test_image│ │scan_image│
|
||||
│ pull+CST │ │pull+Scout│
|
||||
│ ~5m │ │ ~9m │
|
||||
└──────────┘ └──────────┘
|
||||
┌──────────┐ ┌──────────────┐
|
||||
│test_image│ │ scan_image │
|
||||
│ pull+CST │ │ Scout(reg://)│
|
||||
│ ~5m │ │ ~9m │
|
||||
└──────────┘ └──────────────┘
|
||||
|
||||
Total wall: 6m + max(5,9) ≈ 15m
|
||||
(vs. 20m → saves ~5m; with p1 ~7m)
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
## 1. Rename `test_image` → `build_image` and strip the validation steps
|
||||
|
||||
- [ ] 1.1 Rename the `test_image` job key in `build.yml` to `build_image`. Update any references in the workflow.
|
||||
- [ ] 1.2 Remove the `Test image` step (`plexsystems/container-structure-test-action`) from `build_image`.
|
||||
- [ ] 1.3 Remove the `Scan with Docker Scout` step from `build_image`.
|
||||
- [ ] 1.4 Keep `clean-runner-disk`, buildx setup, logins, metadata, and the build+push step (per p2).
|
||||
- [ ] 1.5 Drop `permissions.security-events: write` and `permissions.pull-requests: write` from `build_image` — they belong to `scan_image` now.
|
||||
- [x] 1.1 Rename the `test_image` job key in `build.yml` to `build_image`. Update any references in the workflow.
|
||||
- [x] 1.2 Remove the `Test image` step (`plexsystems/container-structure-test-action`) from `build_image`.
|
||||
- [x] 1.3 Remove the `Scan with Docker Scout` step from `build_image`.
|
||||
- [x] 1.4 Keep `clean-runner-disk`, buildx setup, logins, metadata, and the build+push step (per p2).
|
||||
- [x] 1.5 Drop `permissions.security-events: write` and `permissions.pull-requests: write` from `build_image` — they belong to `scan_image` now.
|
||||
|
||||
## 2. Add the new `test_image` consumer job
|
||||
|
||||
- [ ] 2.1 Add a job `test_image` with `needs: build_image`, `runs-on: ubuntu-24.04`, `permissions.contents: read` and `permissions.packages: read`.
|
||||
- [ ] 2.2 Checkout the repo (CST needs `test/android.yml`).
|
||||
- [ ] 2.3 Branch on `needs.build_image.outputs.image_artifact`:
|
||||
- Non-empty (fork PR): run `clean-runner-disk`, `download-artifact`, `gunzip`, `docker load`, then invoke CST against the loaded local tag.
|
||||
- Empty (non-fork): GHCR login (read) + invoke CST against `needs.build_image.outputs.image_ref` directly (CST pulls under the hood).
|
||||
- [ ] 2.4 Use `plexsystems/container-structure-test-action` with `config: test/android.yml` and `image: <ref-or-loaded-tag>` — satisfies spec scenario "Test job runs in parallel with scan job".
|
||||
- [x] 2.1 Add a job `test_image` with `needs: build_image`, `runs-on: ubuntu-24.04`, `permissions.contents: read` and `permissions.packages: read`. Do NOT add `setup-buildx-action` — the job does not build.
|
||||
- [x] 2.2 Checkout the repo (CST needs `test/android.yml`).
|
||||
- [x] 2.3 Branch on `needs.build_image.outputs.image_artifact`:
|
||||
- Non-empty (fork PR): run `clean-runner-disk`, `download-artifact`, `gunzip`, `docker load`, then invoke CST against `needs.build_image.outputs.image_local_tag`.
|
||||
- Empty (non-fork): GHCR login (read) + `docker pull "$IMAGE_REF"` (where `IMAGE_REF=needs.build_image.outputs.image_ref`), then invoke CST against `IMAGE_REF`. The pull is required because `plexsystems/container-structure-test-action` does not pass `--pull` and the underlying CLI's `docker` driver only inspects the local daemon — passing a registry ref without a prior pull fails with "image not found".
|
||||
- [x] 2.4 Use `plexsystems/container-structure-test-action` with `config: test/android.yml` and `image: <ref-or-loaded-tag>` — satisfies spec scenario "Test job runs in parallel with scan job".
|
||||
|
||||
## 3. Add the new `scan_image` consumer job
|
||||
|
||||
- [ ] 3.1 Add a job `scan_image` with `needs: build_image`, `runs-on: ubuntu-24.04`, `permissions.packages: read`, `permissions.pull-requests: write`, `permissions.security-events: write`.
|
||||
- [ ] 3.2 Gate the entire job: `if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository` (Scout's existing fork gate).
|
||||
- [ ] 3.3 Branch on `image_artifact` like the test job. For the registry path, pass `image: registry://<image_ref>` to `docker/scout-action`. For the artifact path, pass `image: local://<loaded-tag>`.
|
||||
- [ ] 3.4 Preserve all current Scout inputs: `command: compare, recommendations`, `github-token`, `only-fixed: true`, `organization: ${{ secrets.DOCKER_HUB_USERNAME }}`, `to-env: prod`.
|
||||
- [ ] 3.5 Remove the inline TODO `# TODO: Parallelize testing and vulnerability scanning` — this change resolves it. Satisfies spec scenario "Scout scan runs in parallel with CST".
|
||||
- [x] 3.1 Add a job `scan_image` with `needs: build_image`, `runs-on: ubuntu-24.04`, `permissions.packages: read`, `permissions.pull-requests: write`, `permissions.security-events: write`. Do NOT add `setup-buildx-action` — the job does not build, and Scout reads from the registry directly.
|
||||
- [x] 3.2 Gate the entire job: `if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository` (Scout's existing fork gate).
|
||||
- [x] 3.3 Branch on `image_artifact`:
|
||||
- Non-empty (artifact path): `clean-runner-disk`, `download-artifact`, `gunzip`, `docker load`, then pass `image: local://<image_local_tag>` to `docker/scout-action`.
|
||||
- Empty (registry path): GHCR login (read), then pass `image: registry://<image_ref>` to `docker/scout-action` — no `docker pull` needed (the `registry://` prefix tells Scout to bypass the local image store).
|
||||
- [x] 3.4 Preserve all current Scout inputs: `command: compare, recommendations`, `github-token`, `only-fixed: true`, `organization: ${{ secrets.DOCKER_HUB_USERNAME }}`, `to-env: prod`.
|
||||
- [x] 3.5 Bump the `docker/scout-action` pin from v1.18.2 → **v1.20.4** (current as of Apr 2026) while rewriting the step. Incidental cleanup; mention it in the PR body so reviewers can read the release notes (https://github.com/docker/scout-action/releases) and grant approval consciously.
|
||||
- [x] 3.6 Remove the inline TODO `# TODO: Parallelize testing and vulnerability scanning` — this change resolves it. Satisfies spec scenario "Scout scan runs in parallel with CST".
|
||||
|
||||
## 4. Branch-protection migration
|
||||
|
||||
- [ ] 4.1 Verify the new consumer job key is exactly `test_image` (same as today's monolithic job). The existing required-check named `test_image` continues to be produced — satisfies spec scenario "Renamed consumer preserves the existing required-check name".
|
||||
- [ ] 4.2 Inspect current required checks: `gh api repos/<owner>/<repo>/branches/main/protection --jq '.required_status_checks.contexts'`. Confirm `test_image` is present; document any other required check that would be affected.
|
||||
- [ ] 4.3 After this PR merges and produces 3 successful runs on `main`, request a repo admin to add `build_image` and `scan_image` as additional required status checks via `gh api -X PATCH repos/<owner>/<repo>/branches/main/protection`. Document the transient scan-merge-gap (see design D3) in the merge commit so the admin treats it as a follow-up rather than discovering it later.
|
||||
- [x] 4.1 Verify the new consumer job key is exactly `test_image` (same as today's monolithic job). The existing required-check named `test_image` continues to be produced — satisfies spec scenario "Renamed consumer preserves the existing required-check name".
|
||||
- [x] 4.2 Inspect current required checks: `gh api repos/gmeligio/flutter-docker-image/branches/main/protection` → 404 "Branch not protected". No required checks are configured on `main` — no migration needed.
|
||||
- [ ] 4.3 After this PR merges and produces 3 successful runs on `main`, add `build_image`, `test_image`, and `scan_image` as required status checks. Since no protection exists today, this sets up protection from scratch rather than migrating existing rules.
|
||||
|
||||
## 5. Verify on a real PR before merge
|
||||
|
||||
|
||||
Reference in New Issue
Block a user