Chapter 38Lesson 02~185 minutes

Production Capstone: Build, Secure, Scale, Observe, and Govern a Complete GitLab Delivery Platform: Implementation and Automation Build-Out

Build a disposable end-to-end delivery platform incrementally: reusable configuration, tests, synthetic security evidence, build-once artifact identity, provenance metadata, and verified staging deployment.

ImplementationReusable CIArtifact identityVerified deployment

Learning objectives

  • Create a guarded disposable source repository and versioned local reusable CI contract.
  • Explain how pipeline rules, jobs, artifacts and environments change separate GitLab states.
  • Generate test/security evidence and one build-once release artifact with SHA-256 identity.
  • Promote the exact artifact identity into a simulated staging target and independently verify it.
  • Map each local state transition to the evidence a real GitLab project should retain.

Mandatory path: the build-out below is fully disposable and runs with Git, Python 3.10+ and standard shell tools. It produces synthetic source, test/security evidence, a release bundle, a digest/provenance record and a simulated staging target. The GitLab YAML is taught as configuration, while the local harness proves the same state transitions without requiring a GitLab account, paid tier, runner registration, cloud account or real secret.

1. Build the platform incrementally

Atlas Relay needs a minimal delivery contract: validate source, test it, produce security/evidence records, build one immutable release bundle, and deploy that exact bundle to a simulated staging target. We will keep the YAML small and split reusable logic into a local include so configuration provenance is visible instead of copied into every project.

The local harness deliberately mirrors the pipeline stages. Each stage writes evidence before the next stage consumes it. This makes the lab useful even when no GitLab service is available.

2. Create a guarded disposable repository

LAB="${TMPDIR:-/tmp}/gitlab-ch38-capstone"
rm -rf "$LAB"
mkdir -p "$LAB"/{app,tests,ci,scripts,evidence,dist,target,governance}
cd "$LAB"
git init -q
git config user.name 'Chapter 38 Learner'
git config user.email 'learner@example.invalid'

cat > app/relay.py <<'PYAPP'
def normalize(message: str) -> str:
    return ' '.join(message.strip().split()).lower()

if __name__ == '__main__':
    print(normalize('  Atlas   Relay  '))
PYAPP

cat > tests/test_relay.py <<'PYTEST'
import unittest
from app.relay import normalize
class RelayTest(unittest.TestCase):
    def test_normalize(self):
        self.assertEqual(normalize('  Hello   CI  '), 'hello ci')
if __name__ == '__main__': unittest.main()
PYTEST

cat > governance/exception-register.json <<'JSON'
{"exceptions":[],"schema":"owner + reason + expires_at + approved_scope"}
JSON

git add .
git commit -qm 'capstone: establish Atlas Relay source'
printf 'source_sha=%s\n' "$(git rev-parse HEAD)" | tee evidence/00-source.env

3. Add a versioned reusable configuration contract

Current GitLab favors explicit reusable contracts: CI/CD components and spec:inputs are available across tiers, and inputs are typed/validated at pipeline creation. For this repository-local exercise we use include:local, because it is free, reviewable and does not require publishing a component. In production, a cross-project include or catalog component should be pinned to a reviewed tag or commit SHA where practical rather than a moving branch.

# ci/base.yml
.default_job:
  interruptible: true
  before_script:
    - printf 'source=%s pipeline=%s job=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" "$CI_JOB_ID"

# .gitlab-ci.yml
include:
  - local: ci/base.yml

stages: [validate, test, evidence, package, deploy]

default:
  retry:
    max: 1
    when: [runner_system_failure]

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

validate-config:
  extends: .default_job
  stage: validate
  script:
    - python -m py_compile app/relay.py

unit-test:
  extends: .default_job
  stage: test
  script:
    - python -m unittest discover -s tests -v
  artifacts:
    when: always
    expire_in: 1 week
    paths: [evidence/]

build-release:
  extends: .default_job
  stage: package
  script:
    - ./scripts/build_release.sh
  artifacts:
    access: developer
    expire_in: 1 week
    paths: [dist/, evidence/]

deploy-staging:
  extends: .default_job
  stage: deploy
  needs: [build-release]
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
  script:
    - ./scripts/deploy_staging.sh
  environment:
    name: staging

State changed: the repository now contains the pipeline contract. workflow:rules controls pipeline creation; job stages control execution order; artifact settings control retained GitLab-owned evidence; the environment keyword creates GitLab deployment/environment state if run on GitLab. None of these settings proves a runner, external target, or policy exists.

4. Implement build-once and target verification scripts

The release script writes one tar archive, computes its SHA-256, and records source identity. The deployment script does not rebuild; it copies the already-built artifact identity into the simulated target and then reads it back. This is artifact promotion rather than “rebuild on deploy.”

cat > scripts/build_release.sh <<'SH'
#!/usr/bin/env sh
set -eu
mkdir -p dist evidence
SOURCE_SHA="${CI_COMMIT_SHA:-$(git rev-parse HEAD)}"
tar -czf dist/atlas-relay.tgz app tests
DIGEST="$(sha256sum dist/atlas-relay.tgz | awk '{print $1}')"
printf '%s  %s\n' "$DIGEST" dist/atlas-relay.tgz > dist/SHA256SUMS
cat > evidence/provenance.json <<EOF
{"subject":"dist/atlas-relay.tgz","sha256":"$DIGEST","source_sha":"$SOURCE_SHA","builder":"capstone-local-v1"}
EOF
printf 'artifact_digest=%s\n' "$DIGEST" | tee evidence/20-artifact.env
SH

cat > scripts/deploy_staging.sh <<'SH'
#!/usr/bin/env sh
set -eu
mkdir -p target evidence
DIGEST="$(awk '{print $1}' dist/SHA256SUMS)"
SOURCE_SHA="${CI_COMMIT_SHA:-$(git rev-parse HEAD)}"
cat > target/staging.json <<EOF
{"environment":"staging","source_sha":"$SOURCE_SHA","artifact_digest":"sha256:$DIGEST","healthy":true}
EOF
python - <<'PY'
import json
x=json.load(open('target/staging.json'))
assert x['healthy'] is True
assert x['artifact_digest'].startswith('sha256:')
print(x)
PY
cp target/staging.json evidence/30-staging-verified.json
SH
chmod +x scripts/*.sh

5. Generate test and synthetic security/SBOM evidence

A production system may use GitLab security templates, third-party scanners, SARIF, CycloneDX or registry metadata. Those feature and UI boundaries vary by tier. The mandatory lab instead creates small transparent files: a unittest trace, a dependency inventory and a synthetic finding document. The point is not to imitate a scanner; it is to prove the pipeline can retain evidence with source identity.

python -m unittest discover -s tests -v 2>&1 | tee evidence/10-tests.log
python - <<'PY'
import json, sys
json.dump({
  'python': sys.version.split()[0],
  'dependencies': [],
  'format': 'training-inventory-v1'
}, open('evidence/11-dependencies.json','w'), indent=2)
json.dump({
  'scanner': 'synthetic-capstone-check',
  'critical': 0,
  'high': 0,
  'note': 'training evidence; not a vulnerability scanner'
}, open('evidence/12-security.json','w'), indent=2)
PY

6. Run the end-to-end local pipeline simulation

Before running it, predict these changes: the source SHA should stay constant; tests should produce evidence but not mutate the target; build should create exactly one archive and digest; deploy should copy that identity into target state; the final verification must match source and digest independently.

set -eu
SOURCE_SHA="$(git rev-parse HEAD)"
printf 'prediction_source_sha=%s\n' "$SOURCE_SHA" > evidence/predictions.env

python -m py_compile app/relay.py
python -m unittest discover -s tests -v 2>&1 | tee evidence/10-tests.log
./scripts/build_release.sh
./scripts/deploy_staging.sh

ARTIFACT_DIGEST="$(awk '{print $1}' dist/SHA256SUMS)"
python - <<PY | tee evidence/40-final-verification.json
import json
x=json.load(open('target/staging.json'))
expected_sha='$SOURCE_SHA'
expected_digest='sha256:$ARTIFACT_DIGEST'
result={
 'source_matches': x['source_sha']==expected_sha,
 'digest_matches': x['artifact_digest']==expected_digest,
 'healthy': x['healthy'] is True,
 'target': x,
}
print(json.dumps(result,indent=2))
assert all([result['source_matches'],result['digest_matches'],result['healthy']])
PY

7. Map each local transition to real GitLab evidence

Local proof Real GitLab equivalent What to record
git rev-parse HEAD Pipeline source/ref/SHA Project path, pipeline source, ref and exact SHA
Local YAML + include CI Lint / merged configuration / component resolution Lint response, merged config, component/include ref and inputs
Local shell process Runner job execution Pipeline/job IDs, runner ID/version/tags, executor/image/tool versions
Test/security JSON Artifacts/reports/security ingestion Job ID, report type/path, artifact ID/expiry/access and UI ingestion result
Tar + SHA256 Artifact/package/registry object Immutable digest, producer job/SHA and promotion record
Target JSON Environment/deployment + external target Deployment/environment ID, requested digest, target-reported digest/health
Evidence folder Audit/incident evidence packet Timestamps, API responses, assumptions, policy/exception evidence

8. Challenge: choose the failing layer

The build job reports success and the SHA-256 file exists, but the staging target still reports the previous digest. Which layer should you inspect first? The correct answer is deployment/external-state verification, not source compilation, runner tags, or test rules. Preserve the successful build artifact and producer job evidence; then inspect the deployment record, target API/state and promotion logic before deciding whether to retry anything.

9. Bounded cleanup

The lab contains no external side effects. When you finish the full chapter, remove only the guarded workspace after archiving the evidence packet you want to keep.

# Run only after completing Lesson 5 and copying evidence you want to retain.
case "$LAB" in
  */gitlab-ch38-capstone) rm -rf "$LAB" ;;
  *) echo 'Refusing cleanup: unexpected LAB path' >&2; exit 2 ;;
esac

10. Lesson summary

You now have a coherent free/disposable delivery path: versioned repository configuration, reusable local CI logic, tests, synthetic security evidence, build-once artifact identity, provenance-shaped metadata, staging target state and independent verification. Lesson 3 subjects that design to security, governance and reliability validation rather than assuming “green pipeline” means “production ready.”

Next lesson

Next: Production Capstone: Build, Secure, Scale, Observe, and Govern a Complete GitLab Delivery Platform: Security, Governance, and Reliability Validation

Continue with the next lesson in the course sequence and carry forward the evidence-first GitLab CI/CD operating model.

Knowledge check

What makes the capstone platform independently verifiable rather than merely “green”?

If the pipeline succeeds but the external target is unhealthy, what conclusion is valid?

What belongs in the final operational handoff after the capstone?

Version and compatibility note

GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.

Official references and version notes

Further reading — current official GitLab sources

Version-sensitive assumptions in this production capstone were checked against current official GitLab documentation on 2026-09-13. Re-check your exact GitLab, GitLab Runner, glab, executor, component, image/tool and external-provider versions before applying the operating model to production.

Current assumptions used in this chapter: Version-sensitive YAML, Runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations must be verified against the exact GitLab, GitLab Runner, tool, and external-system versions used in production. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for reproducible work.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.