Test attachments
Attachments keep diagnostic data with the related test result. They do not write this data to captured stdout. Tests and worker-side plugins can attach JSON values, text, bytes, or files that already exist.
Attachment creation
Ask for Greenlight\Artifact\Attachments through constructor injection:
use Greenlight\Attribute\NoExpectations;
use Greenlight\Attribute\Test;
use Greenlight\Artifact\Attachments;
use Greenlight\Sandbox\TemporaryDirectory;
final readonly class DiagnosticAttachmentsTest
{
public function __construct(
private Attachments $attachments,
private TemporaryDirectory $temporary,
) {}
#[Test]
#[NoExpectations]
public function recordsDiagnosticData(): void
{
$this->attachments->value('response.json', [
'status' => 202,
'requestId' => 'request-123',
]);
$this->attachments->text('application.log', 'Order accepted.');
$this->attachments->bytes('trace.bin', "\x00\x01");
$source = $this->temporary->path() . '/export.csv';
\file_put_contents($source, "id,status\n123,accepted\n");
$this->attachments->file('export.csv', $source);
}
}
This example uses fixed diagnostic values. Replace them with values from the system under test. The test has no expectations because it only demonstrates attachment creation. The default retention policy discards these attachments when the test passes.
value() encodes its value as JSON. text() and bytes() accept an optional
media type. file() copies a regular file and detects its media type when
possible.
Media types use type/subtype form with optional parameters. Greenlight rejects
malformed values and control characters.
Greenlight copies the content before the method returns. You can remove a
temporary source file after file() returns. Later changes to a value or file
do not change the attachment.
By default, Greenlight retains attachments when the final result fails or has an error. It also retains them when the transformation log contains an earlier failed or errored outcome. A change to passed or skipped therefore preserves failure evidence.
To retain an attachment from a result without failure evidence, set its
retention to AttachmentRetention::Always:
use Greenlight\Artifact\AttachmentRetention;
$attachments->text(
'timing.txt',
$timing,
retention: AttachmentRetention::Always,
);
Each retry has separate attachments. Attachments from failed attempts stay on
the final result even if a later attempt passes. The attempt field identifies
the source attempt for each attachment. For the final attempt, the retention
rules above apply to the final result and its transformation log.
Output directory
By default, Greenlight writes retained attachments to a unique run directory
below build/greenlight-artifacts. A run with no retained attachments does not
create an empty directory. Change the parent directory in greenlight.php:
use Greenlight\Config\ArtifactBuilder;
use Greenlight\Config\GreenlightConfig;
return GreenlightConfig::create()
->artifacts(fn (ArtifactBuilder $artifacts) => $artifacts
->directory('build/test-evidence'));
Use --artifacts-dir to override it for one run:
vendor/bin/greenlight run --artifacts-dir=build/ci-evidence
By default, Greenlight does not remove completed run directories. Configure one or more limits to remove old directories after a run completes:
use Greenlight\Config\ArtifactBuilder;
use Greenlight\Config\GreenlightConfig;
return GreenlightConfig::create()
->artifacts(fn (ArtifactBuilder $artifacts) => $artifacts
->maxCompletedRuns(20)
->maxCompletedRunAge(7 * 24 * 60 * 60)
->maxRetainedSize('2G'));
The age value is in seconds. Greenlight applies age, count, and byte limits in that order. Each limit removes the oldest eligible completed run first.
Use greenlight artifacts:prune --dry-run to examine the configured policy.
Use the command without --dry-run to apply the policy.
Metadata and names
Each attachment records its name, kind, media type, byte size, SHA-256 digest, attempt number, retention policy, and published path. The metadata does not include the original source path or the attachment content.
Use a non-empty label for each attachment name. Use valid UTF-8 and no more
than 120 bytes. Names cannot contain directory separators or control characters.
They cannot equal . or ... Repeated names
within one attempt receive -2, -3, and later suffixes in their published
filenames. Their logical names remain unchanged in the result metadata.
Greenlight converts test IDs to slugs and hashes before it uses them in paths.
File safety
Source files must be regular files, not symlinks. Greenlight verifies that a source does not change during the copy operation. Published paths stay in the configured run directory. Greenlight creates artifact files with private permissions on supported platforms.
Each run directory contains versioned ownership metadata and a lifecycle lock. Greenlight prunes only completed directories with an exact content manifest. It does not prune active, incomplete, changed, unknown, or symbolic-link directories. Cleanup claims use atomic directory renames and a parent lock.
Greenlight does not inspect or redact attachment content. Before you attach a value or file, remove secrets and personal data. Apply the policy for sensitive CI artifacts to the output directory.
Limits
The defaults are:
- 32 attachments per test
- 25 MiB per attachment
- 100 MiB across all attachments for one test
- 10,000 attachments per run
- 1 GiB across all attachments for one run
Configure the limits with maxAttachmentsPerTest(), maxAttachmentSize(),
maxTestSize(), maxRunAttachments(), and maxRunSize() on
ArtifactBuilder. Size methods accept values such as 10M and 2G. A limit
violation fails the active test with an attachment error. Greenlight does not
truncate attachment content.
Greenlight coordinates run limits through private shared staging. Thus, they apply across parallel workers. Per-test limits include all attempts, even when Greenlight discards some attachments later. Greenlight releases run quota when it discards an attachment.
Completed run limits are independent of attachment limits. The completed run defaults are unbounded. Retention failures do not fail a test run.
Plugins
$context->attachments gives the same attempt-owned object to
BeforeTestSubscriber::beforeTest() and AfterTestSubscriber::afterTest().
A plugin can attach
data before the test or after it examines the result.
A retry decider receives attachment metadata on the TestResult, but it cannot
read the content through the result. See writing plugins for the
plugin interfaces.
Reporters and CI
The tty and plain reporters print the paths of retained attachments. JUnit
adds [[ATTACHMENT|path]] markers to the test case’s system-out. GitHub
annotations include attachment paths and an artifact directory notice. TeamCity
emits artifact metadata and publishes the run directory.
JSONL includes attachment metadata and reports the run directory in
run-started.artifactsDirectory. See the JSONL
schema.
For other CI systems, use a post-test step that always runs. Upload the reported run directory from this step.
CI platform retention remains authoritative after upload. Greenlight retention controls only the runner filesystem.