Pipeline model as one provider’s YAML. The three built-in drivers (GitHubDriver, BitbucketDriver, GitLabDriver) are the reference implementations; read whichever is closest to the provider you target.
The contract
render() returns the body only. The compiler adds the “generated, do not edit” header, scans the result for credentials, and decides the file path.
The model you render
Pipeline:name,triggers,jobs(in execution order),environments,declaredSecrets,cancelSuperseded,notifications. Helpers:stages(),job($id),jobsFor($event, $branch).Job:id,name,stage,needs,phpVersions(more than one = matrix),extensions,coverage,node,tools,services,caches,steps,timeoutMinutes,environment,manual(needs approval),events,branches,tags,artifactPaths,consumesArtifact,env.Step:type(Run,ArtifactUpload,ArtifactDownload),name,run(POSIXsh),env,secrets(names),runOn(success|failure|always).Triggers: push branches, tag patterns, pull-request on/off and target branches, cron schedule, manual, ignored paths.
needs is acyclic.
Capabilities
Return aSupport level for every Feature case (Matrix, Services, Approvals, Environments, Concurrency, Schedule, ManualTrigger, Artifacts, PathFilters, Timeouts, Caching, ParallelJobs, TagTriggers). A missing entry is treated as None.
Full: expressed natively.Partial: expressed with a caveat. Explain the caveat innotes(), including any one-time setting the user must make in the provider’s UI. It becomes a compile warning.None: cannot be expressed. It is left out with a warning, or is a compile error instrictmode.
Full you cannot back up is worse than a Partial with a clear note.
Rules every driver must follow
1
Deterministic
The same pipeline must render the same bytes. No clocks, randomness or unordered maps.
2
Secrets by name
Never write a secret value. Expose a step’s
secrets to its script as environment variables of the same name, using the provider’s secret reference syntax. Do not interpolate secrets into command text.3
Reject unknown options
AbstractDriver::assertOptions($options, ['allowed', 'keys']) throws InvalidArgumentException; the compiler reports it as drivers.<name>: ….4
Honour raw
The driver’s
raw option is deep-merged over the generated structure (AbstractDriver::deepMerge: maps merge; lists and scalars replace).5
Fail fast in multi-line scripts
Providers that run a list of script lines can ignore a failure in the middle of a block.
AbstractDriver::failFast() prefixes set -e.6
No tabs, no trailing whitespace
Keep the output clean so reviews and byte comparisons stay reliable.
7
Use the YAML emitter
Support\Yaml::dump($array) quotes values that YAML 1.1 would misread (y, no, on, 1e3, 08, and so on) and uses literal block scalars for multi-line strings. Build arrays, not strings.AbstractDriver also provides jobEnv(), globPath(), bareDir(), stringOption(), assertNoExpressions(), successScripts(), containerSetup(), nodeInstall(), shellExports() and waitForPorts() for providers that run jobs in plain containers.
Skeleton
'drivers' => ['circleci' => true].
Prove it with the conformance suite
tests/Conformance/DriverConformanceTestCase.php encodes the rules above as tests: identity, a support level for every feature, every Partial/None explained, deterministic output, no tabs or trailing whitespace, secrets never inlined, every job present in the output, unknown options rejected, raw applied, and registration with the DriverManager.
The case lives in this package’s tests/ directory, so Composer does not autoload it into dependent packages. Copy DriverConformanceTestCase.php and tests/Support/Fixture.php into your driver’s own tests (adjust the namespaces), then: