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
14 changes: 10 additions & 4 deletions .devcontainer/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
FROM node:22.23.4-bookworm-slim@sha256:4b150b48b4116e2a22f2c39cc27025fbf4f153e2078fd3b14d159fdb5c7279da AS node

Check warning on line 1 in .devcontainer/docker/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaGtuXRT6XUfQ6iTPts0&open=AaGtuXRT6XUfQ6iTPts0&pullRequest=323

FROM composer:2.11.0@sha256:278dfc7bd35169965d03f87194816f43bf4e6b6ea61707c2114e52ad12fe24ce AS composer

Check warning on line 3 in .devcontainer/docker/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaGtuXRT6XUfQ6iTPts1&open=AaGtuXRT6XUfQ6iTPts1&pullRequest=323

FROM php:8.4.27-cli-bookworm@sha256:54b467ede1db96c66675e7b78457dd43729d0e7be31978d4a1e5c069ceabcf76 AS brotli-build
FROM php:8.4.27-cli-bookworm@sha256:b3e907a1108629c562c4bb313ce3449e34d49991f6bb2487913f6146f47200fe AS brotli-build

Check warning on line 5 in .devcontainer/docker/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaGtuXRT6XUfQ6iTPts2&open=AaGtuXRT6XUfQ6iTPts2&pullRequest=323

COPY .central-apt-source/docker/apt/require-fixed-bookworm-apt.sh /usr/local/share/sample-app/require-fixed-bookworm-apt.sh
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
COPY .devcontainer/docker/verify-php-trust.php /usr/local/share/sample-app/verify-php-trust.php
COPY docker/brotli/version docker/brotli/install-vendor.sh docker/brotli/verify-runtime.php docker/brotli/verify-system-brotli.c /usr/src/brotli-inputs/
RUN php /usr/local/share/sample-app/verify-php-trust.php /usr/local/bin/composer \
RUN /bin/sh /usr/local/share/sample-app/require-fixed-bookworm-apt.sh \
&& php /usr/local/share/sample-app/verify-php-trust.php /usr/local/bin/composer \
&& sh /usr/src/brotli-inputs/install-vendor.sh /usr/src/brotli-inputs gcc libc6-dev \
&& mkdir -p /out \
&& cc -O2 /usr/src/brotli-inputs/verify-system-brotli.c -lbrotlidec -lbrotlienc -ldl -o /out/verify-system-brotli \
&& /out/verify-system-brotli \
&& cp /usr/src/brotli-inputs/version /usr/src/brotli-inputs/install-vendor.sh /usr/src/brotli-inputs/verify-runtime.php /out/

FROM docker:29.9.2-cli@sha256:5e6baf67325a79f64ed3bed6863954d5e05d0f3d4e57cbdb14809346814bbfdb AS docker-cli

Check warning on line 19 in .devcontainer/docker/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaGtuXRT6XUfQ6iTPts3&open=AaGtuXRT6XUfQ6iTPts3&pullRequest=323

FROM golang:1.27.3-bookworm@sha256:d9b6a1beb9474adc2a1ce842573639b44b2a0dd49b6669f51d3821baf9ec37c9 AS go-tools

Check warning on line 21 in .devcontainer/docker/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaGtuXRT6XUfQ6iTPts4&open=AaGtuXRT6XUfQ6iTPts4&pullRequest=323

ENV CGO_ENABLED=0 GOTOOLCHAIN=local
COPY docker/go-tools/compose-go-net.patch /tmp/compose-go-net.patch
Expand Down Expand Up @@ -47,7 +49,7 @@
go version -m /out/docker-compose; \
/out/docker-compose version

FROM rust:1.99.1-slim-bookworm@sha256:24707e70a4f1e23d9a7b25202ec43126d28a52294b96aad5c670df1fd1f1d400 AS rust

Check warning on line 52 in .devcontainer/docker/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaGtuXRT6XUfQ6iTPts5&open=AaGtuXRT6XUfQ6iTPts5&pullRequest=323

RUN set -eux; \
rm -rf /usr/local/rustup/downloads /usr/local/rustup/tmp; \
Expand All @@ -57,9 +59,9 @@
rustc --version; \
cargo --version

FROM mariadb:11.4.13@sha256:d1891ede5a559f1086f55053bb8cc1569b0298338c4d72d71725a7f74f6ba620 AS mysql-seed

Check warning on line 62 in .devcontainer/docker/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaGtuXRT6XUfQ6iTPts6&open=AaGtuXRT6XUfQ6iTPts6&pullRequest=323

ENV MYSQL_ROOT_PASSWORD=password \

Check warning on line 64 in .devcontainer/docker/Dockerfile

View workflow job for this annotation

GitHub Actions / Qualify candidate linux/amd64

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "MYSQL_PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 64 in .devcontainer/docker/Dockerfile

View workflow job for this annotation

GitHub Actions / Qualify candidate linux/amd64

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "MYSQL_ROOT_PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 64 in .devcontainer/docker/Dockerfile

View workflow job for this annotation

GitHub Actions / Qualify candidate linux/arm64

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "MYSQL_PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 64 in .devcontainer/docker/Dockerfile

View workflow job for this annotation

GitHub Actions / Qualify candidate linux/arm64

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "MYSQL_ROOT_PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
MYSQL_ROOT_HOST=% \
MYSQL_DATABASE=sample \
MYSQL_USER=laravel \
Expand Down Expand Up @@ -127,10 +129,12 @@
.; \
test -s /tmp/sample-app-mysql-seed.tar

FROM php:8.4.27-cli-bookworm@sha256:54b467ede1db96c66675e7b78457dd43729d0e7be31978d4a1e5c069ceabcf76
FROM php:8.4.27-cli-bookworm@sha256:b3e907a1108629c562c4bb313ce3449e34d49991f6bb2487913f6146f47200fe

Check warning on line 132 in .devcontainer/docker/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use either the version tag or the digest for the image instead of both.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaGtuXRT6XUfQ6iTPts7&open=AaGtuXRT6XUfQ6iTPts7&pullRequest=323

COPY .central-apt-source/docker/apt/require-fixed-bookworm-apt.sh /usr/local/share/sample-app/require-fixed-bookworm-apt.sh
COPY --from=brotli-build /out /usr/local/share/sample-app/brotli
RUN sh /usr/local/share/sample-app/brotli/install-vendor.sh /usr/local/share/sample-app/brotli \
RUN /bin/sh /usr/local/share/sample-app/require-fixed-bookworm-apt.sh \
&& sh /usr/local/share/sample-app/brotli/install-vendor.sh /usr/local/share/sample-app/brotli \
&& php /usr/local/share/sample-app/brotli/verify-runtime.php \
&& rm -rf /var/lib/apt/lists/*

Expand Down Expand Up @@ -174,6 +178,7 @@
php /usr/local/share/sample-app/verify-php-trust.php "$(command -v composer)"; \
ln -snf "/usr/share/zoneinfo/${TZ}" /etc/localtime; \
echo "${TZ}" > /etc/timezone; \
/bin/sh /usr/local/share/sample-app/require-fixed-bookworm-apt.sh; \
apt-get update; \
apt-get upgrade -y --no-install-recommends; \
apt-get install -y --no-install-recommends \
Expand Down Expand Up @@ -357,6 +362,7 @@
playwright_version="$(node -e 'const lock = require("/tmp/sample-app-package-lock.json"); process.stdout.write(lock.packages["node_modules/playwright"].version)')"; \
test -n "$playwright_version"; \
npm install --global --no-audit --no-fund "playwright@${playwright_version}"; \
/bin/sh /usr/local/share/sample-app/require-fixed-bookworm-apt.sh; \
PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT=120000 playwright install --with-deps --only-shell chromium; \
python3 /usr/local/share/sample-app/python/build-pip.py --clean-seeds; \
rm /usr/local/share/sample-app/python/build-pip.py /usr/local/share/sample-app/python/pip-urllib3.patch; \
Expand Down
31 changes: 31 additions & 0 deletions .devcontainer/docker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Development image build inputs

The development recipe uses the same pinned PHP 8.4.27 Bookworm index for its
Brotli builder and final stage, on both amd64 and arm64. Its shared APT preflight
comes from the accepted immutable source in
[central `docker/apt`](https://github.com/durable-workflow/.github/tree/18783be6bef737cc778e4c7282a8797e39daa61a/docker/apt).
That source records the exact parent descriptors and publisher SPDX package
inventory; installed-image acceptance is a separate result.

Both image workflows check out that exact central commit without persisted
credentials into `.central-apt-source`, then verify its actual HEAD and the
preflight file's SHA-256 before preparing Buildx. The disposable checkout is
Git-ignored; its Git metadata is excluded from the Docker context. The Dockerfile
copies the central file directly from that local context into each PHP stage.
A missing checkout or file fails the build; there is no external-image fallback
or repository-local guard implementation.

The preflight runs before each stage's first RUN reaches the Brotli APT helper,
then again before the final stage's direct APT block and Playwright dependency
installation. It uses stock shell/dpkg and needs no Python or package acquisition
to establish the installed native APT/libapt pair.
The shared startup qualification invokes it again inside the completed image
after application preparation, retaining an explicit final installed-pair result.

The PR workflow builds untrusted native candidates with publication disabled.
The seven-job publisher remains a separate explicit main-only dispatch, including
its existing scan, anonymous/native qualification and promotion gates. Publisher
inventory does not establish installed-image acceptance. Publication and
production deployment retain their separate decisions. See
[#318](https://github.com/durable-workflow/sample-app/issues/318) for the current
adoption and effect decisions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules
vendor
.git
.central-apt-source/.git
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/devcontainer-image-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check out the accepted central APT preflight
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: durable-workflow/.github
ref: 18783be6bef737cc778e4c7282a8797e39daa61a
path: .central-apt-source
persist-credentials: false
- name: Verify the central APT source identity
run: |
test "$(git -C .central-apt-source rev-parse HEAD)" = 18783be6bef737cc778e4c7282a8797e39daa61a
echo '2a36a2417e8947357ea6f6cbc2f6c53a834f3c069f8013a34d6d861b7123d576 .central-apt-source/docker/apt/require-fixed-bookworm-apt.sh' | sha256sum --check --strict
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- name: Build untrusted candidate without a shared cache
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/devcontainer-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check out the accepted central APT preflight
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: durable-workflow/.github
ref: 18783be6bef737cc778e4c7282a8797e39daa61a
path: .central-apt-source
persist-credentials: false
- name: Verify the central APT source identity
run: |
test "$(git -C .central-apt-source rev-parse HEAD)" = 18783be6bef737cc778e4c7282a8797e39daa61a
echo '2a36a2417e8947357ea6f6cbc2f6c53a834f3c069f8013a34d6d861b7123d576 .central-apt-source/docker/apt/require-fixed-bookworm-apt.sh' | sha256sum --check --strict
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- name: Sign in to GitHub Container Registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/storage/pail
/vendor
/.playground
/.central-apt-source/
/playground/php-runtime/vendor
.env
.env.backup
Expand Down
1 change: 1 addition & 0 deletions scripts/ci/qualify-devcontainer-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ run_in_ready_devcontainer laravel .devcontainer/post-create.sh
"${compose[@]}" up --detach --no-build --wait

# The complete image inventory includes root-owned build sources and tools.
"${compose[@]}" exec -T --user root laravel /bin/sh /usr/local/share/sample-app/require-fixed-bookworm-apt.sh
"${compose[@]}" exec -T --user root laravel php /usr/local/share/sample-app/brotli/verify-runtime.php

run_in_ready_devcontainer laravel bash -euc '
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/BrotliImageContractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function test_both_images_install_same_base_packages_before_network_clien
$this->assertCount(2, $runtimes[1]);
$this->assertSame($builder[1], $runtimes[1][1]);
$runtime = substr($source, strrpos($source, "\nFROM php:"));
$install = strpos($runtime, 'RUN sh /usr/local/share/sample-app/brotli/install-vendor.sh ');
$install = strpos($runtime, 'sh /usr/local/share/sample-app/brotli/install-vendor.sh ');
$check = strpos($runtime, 'php /usr/local/share/sample-app/brotli/verify-runtime.php');
$download = strpos($runtime, 'apt-get update');
foreach ([$install, $check, $download] as $position) {
Expand Down
25 changes: 25 additions & 0 deletions tests/Unit/DevcontainerImageContractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,31 @@ public function test_composer_jobs_check_trust_before_dependency_resolution(): v
}
}

public function test_image_builds_bind_the_shared_preflight_before_buildx(): void
{
$commit = '18783be6bef737cc778e4c7282a8797e39daa61a';
$hash = '2a36a2417e8947357ea6f6cbc2f6c53a834f3c069f8013a34d6d861b7123d576';
foreach (['devcontainer-image-pr.yml' => 'qualify', 'devcontainer-image.yml' => 'build'] as $file => $job) {
$steps = Yaml::parseFile($this->path('.github/workflows/'.$file))['jobs'][$job]['steps'];
$checkouts = array_filter($steps, static fn (array $step): bool => ($step['with']['repository'] ?? null) === 'durable-workflow/.github');
$this->assertCount(1, $checkouts);
$index = array_key_first($checkouts);
$this->assertSame('actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1', $steps[$index]['uses']);
$this->assertSame($commit, $steps[$index]['with']['ref']);
$this->assertSame('.central-apt-source', $steps[$index]['with']['path']);
$this->assertFalse($steps[$index]['with']['persist-credentials']);
$verification = $steps[$index + 1]['run'];
$this->assertStringContainsString('test "$(git -C .central-apt-source rev-parse HEAD)" = '.$commit, $verification);
$this->assertStringContainsString($hash.' .central-apt-source/docker/apt/require-fixed-bookworm-apt.sh', $verification);
$this->assertStringContainsString('sha256sum --check --strict', $verification);
foreach ($steps as $position => $step) {
if (str_starts_with($step['uses'] ?? '', 'docker/setup-buildx-action@')) {
$this->assertLessThan($position, $index + 1);
}
}
}
}

public function test_main_publication_builds_both_architectures_before_promotion(): void
{
$workflow = Yaml::parseFile($this->path('.github/workflows/devcontainer-image.yml'));
Expand Down
101 changes: 101 additions & 0 deletions tests/ci/test_bookworm_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import fnmatch
import pathlib
import re
import unittest


ROOT = pathlib.Path(__file__).resolve().parents[2]
CONTEXT_SOURCE = ".central-apt-source/docker/apt/require-fixed-bookworm-apt.sh"
GUARD_PATH = "/usr/local/share/sample-app/require-fixed-bookworm-apt.sh"
GUARD = "/bin/sh " + GUARD_PATH
PARENT = "php:8.4.27-cli-bookworm@sha256:b3e907a1108629c562c4bb313ce3449e34d49991f6bb2487913f6146f47200fe"


def instructions(source):
pending = ""
for line in source.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if line.endswith("\\"):
pending += line[:-1] + " "
continue
instruction = pending + line
pending = ""
operation, _, argument = instruction.partition(" ")
yield operation, argument.strip()


def php_stages(source):
stages = []
current = None
for operation, argument in instructions(source):
if operation == "FROM":
current = [] if argument.startswith("php:") else None
if current is not None:
assert argument.split()[0] == PARENT, "Unexpected PHP parent"
stages.append(current)
elif current is not None:
current.append((operation, argument))
assert len(stages) == 2, "Both PHP stages must use the accepted parent"
return stages


def protected_php_stages(source):
for stage in php_stages(source):
copy = stage.index(("COPY", CONTEXT_SOURCE + " " + GUARD_PATH))
first_run = next(index for index, (operation, _) in enumerate(stage) if operation == "RUN")
assert copy < first_run, "The central local-context COPY must precede the first RUN"
assert stage[first_run][1].startswith(GUARD), "The first RUN must start with the central preflight"
for operation, argument in stage:
if operation != "RUN":
continue
for boundary in re.finditer(r"\bapt-get\s+(?:update|upgrade|install|purge)\b|\bplaywright\s+install\b[^;]*--with-deps", argument):
assert argument.rfind(GUARD, 0, boundary.start()) >= 0, "APT boundary has no preceding preflight in its RUN"


class BookwormConsumerTests(unittest.TestCase):
def setUp(self):
self.source = (ROOT / ".devcontainer/docker/Dockerfile").read_text()

def test_both_php_stages_guard_their_apt_boundaries(self):
protected_php_stages(self.source)

def test_an_early_brotli_install_cannot_precede_the_guard(self):
continuation = " " + chr(92) + chr(10) + " "
bad = self.source.replace("RUN " + GUARD + continuation + "&& ", "RUN ", 1)
self.assertTrue(self.source != bad, "The early-guard mutation must change the source")

Check warning on line 67 in tests/ci/test_bookworm_consumer.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using "assertNotEqual" instead.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaGtuXLj6XUfQ6iTPtsy&open=AaGtuXLj6XUfQ6iTPtsy&pullRequest=323
with self.assertRaisesRegex(AssertionError, "first RUN"):
protected_php_stages(bad)

def test_later_apt_and_playwright_guards_cannot_be_removed(self):
continuation = " " + chr(92) + chr(10) + " "
for following in ["apt-get update", "PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT=120000 playwright install"]:
with self.subTest(boundary=following):
bad = self.source.replace(GUARD + ";" + continuation + following, following, 1)
self.assertTrue(self.source != bad, "The later-guard mutation must change the source")

Check warning on line 76 in tests/ci/test_bookworm_consumer.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using "assertNotEqual" instead.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sample-app&issues=AaGtuXLj6XUfQ6iTPtsz&open=AaGtuXLj6XUfQ6iTPtsz&pullRequest=323
with self.assertRaisesRegex(AssertionError, "APT boundary"):
protected_php_stages(bad)

def test_missing_central_context_has_no_external_copy_fallback(self):
bad = self.source.replace("COPY " + CONTEXT_SOURCE, "COPY --from=unreviewed-guard " + CONTEXT_SOURCE, 1)
with self.assertRaises(ValueError):
protected_php_stages(bad)

def test_the_guard_is_in_the_build_context_but_git_metadata_is_not(self):
patterns = [line.strip().rstrip("/") for line in (ROOT / ".dockerignore").read_text().splitlines() if line.strip() and not line.startswith("#")]
ancestors = [str(path) for path in pathlib.PurePosixPath(CONTEXT_SOURCE).parents]
for pattern in patterns:
self.assertFalse(any(fnmatch.fnmatchcase(path, pattern) for path in [CONTEXT_SOURCE, *ancestors]), pattern)
self.assertIn(".central-apt-source/.git", patterns)
self.assertIn("/.central-apt-source/", (ROOT / ".gitignore").read_text().splitlines())

def test_completed_image_qualification_rechecks_the_installed_pair(self):
qualification = (ROOT / "scripts/ci/qualify-devcontainer-image.sh").read_text()
invocation = '"${compose[@]}" exec -T --user root laravel ' + GUARD
self.assertIn(invocation, qualification)
self.assertGreater(qualification.index(invocation), qualification.index("run_in_ready_devcontainer laravel .devcontainer/post-create.sh"))


if __name__ == "__main__":
unittest.main()
Loading