Bassam Ismail
Replacing Split CI/CD With a Verified CodePipeline-to-Acquia Deployment Path
Engineering

Replacing Split CI/CD With a Verified CodePipeline-to-Acquia Deployment Path

8 min read

Two green checks appeared on the same pull request. One came from the GitHub Actions workflow we were retiring, the other from its CodePipeline replacement. Both claimed the change was safe, but only one had waited to see whether the deployment actually succeeded.

We were replacing two delivery paths with a CodePipeline path that reported the remote Acquia result. We made the CLI deployment synchronous, compared the replacement with the old workflows in parallel, and removed each old workflow only after its replacement had produced observable evidence.

TL;DR

The migration was a transfer of authority across a trust boundary. CodeBuild took responsibility for pull request checks, artifact upload, and the Acquia environment switch. --task-wait made the deployment stage wait for the remote result, while a Verify stage ran dependency and coding-standard checks before the pipeline could finish successfully. Because Verify followed Deploy, its failure could occur after an artifact was live. That risk requires rollback or traffic control outside the pipeline design described here.

Moving YAML was the easy part. The hard part was deciding which system could authorize a merge or deployment, what evidence supported that authority, and when the former system could be removed.

The trust boundary behind the migration

The repository had accumulated two delivery systems. GitHub Actions performed pull request checks and parts of the deployment workflow. AWS CodePipeline and CodeBuild were being introduced to take over those jobs. During migration, both systems were intentionally active.

That overlap gave us a comparison period, but it was not a stable architecture. We needed unambiguous answers to practical questions:

  • Which failing check blocks a merge?
  • Which pipeline owns deployment?
  • Does a green pipeline mean the remote environment switched code?
  • If two systems deploy, which one produced the version currently running?

During the transition, the answer to the last question could be “both.” After the transfer, that answer would be unacceptable.

OWNERSHIP CHANGESPLIT PATHSINGLE PATHGitHub checksAWS checkscustom deployPR gateCLI deployVerify stage[ One owner per delivery responsibility ]

The diagram records the migration paths, not concurrent ownership in the final state. GitHub checks and AWS checks both fed the PR-gate responsibility during comparison. AWS also supplied the Verify stage. The custom deployment path moved to the CLI deployment. The transfer was complete only when the replacement could show the required evidence and the former path no longer exercised authority.

Assigning responsibility and evidence

“AWS handles CI now” cannot be inspected. “The CodeBuild project reports the required commit status on each pull request” can.

We assigned the responsibilities this way:

ResponsibilityOwner after migrationEvidence
Pull request checksCodeBuild PR gateCommit status on each PR
Artifact uploadAcquia CLI in CodeBuildSuccessful push:artifact exit code
Environment switchAcquia CLI in CodeBuildCompleted remote task
Pipeline acceptanceVerify stageStage succeeds only when checks pass
Legacy automationNoneWorkflow removed after overlap period

One detail mattered more than it first appeared. A PHPUnit job in the old pull request workflow had already been disabled with if: false. Keeping that workflow did not preserve the test coverage its filename implied. It preserved a green check with a historical reputation.

That exposed a common migration error: comparing labels instead of behavior. A workflow called “PR checks” may execute fewer checks than its replacement while looking more familiar in the interface. We compared the responsibility, the observable result, and the authority attached to that result.

Making the Acquia deployment explicit

The previous deployment path used roughly 60 lines of hand-rolled Git artifact plumbing. That code assembled and pushed deployment state through Git operations. It worked, but it also made the pipeline reproduce behavior already provided by the platform CLI.

We replaced that plumbing with two commands:

set -euo pipefail
 
acli push:artifact \
  --destination-git-branch="$ACQUIA_ARTIFACT_BRANCH" \
  --no-interaction
 
acli api:environments:code-switch \
  "$ACQUIA_ENVIRONMENT_ID" \
  "$ACQUIA_ARTIFACT_BRANCH" \
  --task-wait \
  --no-interaction

The first command pushes the prepared artifact. The second asks the target environment to switch to that artifact. The decisive option is --task-wait. CodeBuild remains attached to the remote operation and receives its result instead of treating task acceptance as deployment success.

Without waiting, the sequence can look like this:

  1. The API accepts a code-switch request.
  2. The deployment process exits successfully.
  3. CodePipeline turns green.
  4. The remote task fails several minutes later.

That sequence verifies submission of a deployment request, not deployment.

SYNCHRONOUS DEPLOYMENTpush artifactupload codeartifact readyswitch codestart tasktask resultexit statusCodeBuildCLIplatformCodeBuildCLIplatform[ Pipeline success follows the remote result ]

A representative CodeBuild deployment phase keeps the commands small. Ordinary process exit codes carry failure into CodePipeline:

version: 0.2
 
phases:
  install:
    runtime-versions:
      php: 8.3
  build:
    commands:
      - set -euo pipefail
      - acli push:artifact --destination-git-branch="$ACQUIA_ARTIFACT_BRANCH" --no-interaction
      - acli api:environments:code-switch "$ACQUIA_ENVIRONMENT_ID" "$ACQUIA_ARTIFACT_BRANCH" --task-wait --no-interaction
artifacts:
  files:
    - "**/*"

The shorter implementation was useful, but its failure semantics mattered more. The commands state the deployment intent directly, and their exit codes participate in the pipeline’s control flow.

Important

A deployment stage should succeed when the target operation completes, not when an asynchronous task is merely accepted. If the platform offers a wait option, use it and preserve its exit code.

What Verify proves, and what it does not

Each pipeline included a Verify stage with dependency validation and coding-standard checks appropriate to that path. A failure prevented CodePipeline from presenting the revision as acceptable.

VERIFIED PIPELINESourceselect revisionBuildcreate artifactDeploywait for switchVerifygate success[ Green means the required path completed ]

A required check has authority that a detached reporting workflow does not. If Verify fails, the pipeline fails, so operators and automation receive one result from the delivery path.

The stage order also exposes the architecture’s sharpest unresolved tradeoff. Verify runs after Deploy. Dependency validation and coding-standard checks are artifact-oriented checks that can reveal an unacceptable revision, but by then the environment may already have completed its code switch. Pipeline failure does not itself prove that traffic stopped reaching the artifact or that the environment rolled back.

Those checks should be split by boundary. Validation that can establish artifact safety before promotion belongs before Deploy. Checks that require the switched environment, such as post-deploy health checks, belong after Deploy. A production path also needs defined behavior for a post-deploy failure: automatically restore the previous artifact, keep traffic away from the new version until health checks pass, or stop for an explicit operator action. This implementation does not establish which of those controls existed, so its green result is stronger than task submission but not equivalent to transactional deployment safety.

Extra checks cost execution time and build capacity, and some repeat evidence gathered on the pull request. I would repeat only checks whose failure covers a different risk at the artifact or deployed-environment boundary.

Transferring authority and retiring the old path

We removed the superseded GitHub Actions in two phases. The first phase covered automation whose replacement had already been running in parallel, including the pull request gate. The remaining workflows stayed until their corresponding CodePipeline paths had equivalent evidence.

The reusable sequence was:

  1. Assign a responsibility to the replacement.
  2. Define the result that proves it performed that responsibility.
  3. Run the legacy and replacement paths together long enough to compare behavior, status reporting, and failure propagation.
  4. Transfer authority to the replacement.
  5. Remove the former path covered by that evidence.
  6. Repeat for the remaining responsibilities.

Deleting all legacy workflows after the new pipeline’s first green run would have made the repository tidier and rollback harder. One success proves that a happy path exists. It does not exercise the failure cases needed to justify removing the fallback.

Indefinite parallel operation was not a solution either. The paths would drift and force responders to reconstruct which system acted. Parallel execution was an experiment with an exit criterion.

After deletion, rollback meant restoring code or fixing the new pipeline under pressure. Synchronous waiting also made deployments visibly slower when the remote platform queued tasks. We accepted those costs because a quick success signal that precedes the remote result is operationally misleading.

A green pipeline is therefore a bounded claim about what the system observed. In this path, it meant the owned checks passed, the artifact was pushed, Acquia completed the code switch, and Verify accepted the result. It did not establish automatic rollback or traffic isolation after a post-deploy verification failure. That remaining boundary must be designed explicitly before green can mean the deployed artifact is safe to serve.

FAQ

How do I consolidate split CI/CD safely?

Assign one owner to each responsibility, run the replacement beside the legacy path, and compare observable outcomes before deletion. Retire workflows in phases based on proven coverage, not by file category.

Why use acli push:artifact for Acquia deployment?

acli push:artifact expresses artifact promotion directly and replaces custom Git plumbing that the platform CLI already handles. It also keeps the deployment procedure easier to inspect in CodeBuild logs.

Why does api:environments:code-switch need --task-wait?

Without --task-wait, the command may return after the remote task is accepted rather than completed. Waiting allows a failed code switch to produce a failed build and pipeline.

Where should deployment verification run in CodePipeline?

Put verification in a dedicated stage on the required path to pipeline success. A detached check can report a failure while the delivery pipeline remains green.

When should old GitHub Actions workflows be removed?

Remove each workflow after its replacement has run in parallel and demonstrated equivalent or better coverage, status reporting, and failure propagation. Do not keep both paths indefinitely, because their behavior will diverge.

The replacement earned authority by reporting what it could observe. The gap between a failed post-deploy check and a safe rollback remained a separate engineering obligation.

More to read