Checkpoint Lab — Multi-Project Pipelines, Downstream Triggers, Cross-Project Dependencies, and Platform-Oriented Delivery
The checkpoint lab builds a two-project delivery chain with a narrow job-token allowlist, verifies the exact downstream SHA and mirrored status, injects one authorization failure, repairs only the access edge, and produces an evidence packet suitable for platform review.
Learning objectives
- Build a two-project pipeline using a governed downstream ref and strategy: mirror.
- Use a narrow CI_JOB_TOKEN allowlist and prove one intended API call succeeds while the unauthorized baseline is preserved.
- Verify the exact downstream SHA independently rather than trusting the requested ref name alone.
- Produce a complete cross-project evidence packet including identities, authorization, inputs, statuses, assumptions, and rollback.
- Bridge to Chapter 16 by separating cross-project orchestration from merge-request and merge-train validation semantics.
1. Checkpoint mission
Build a disposable two-project chain in which
ch15-upstream triggers ch15-downstream on
a governed lab tag with strategy: mirror. The
downstream receives only typed non-secret inputs, verifies the
upstream SHA through an allowlisted Free-tier Commit API request,
and records its own exact SHA/status. You must preserve one
intentional authorization denial before repairing it.
2. Assumptions and preflight
- GitLab.com, Self-Managed, or Dedicated with current multi-project pipeline/input syntax; docs checked 2026-09-11.
- Two private disposable projects under a namespace you control.
- A normal non-privileged runner capable of Alpine 3.20.3 jobs, or equivalent trusted disposable runner.
- No production secrets, cloud accounts, registries, packages, releases, environments, or customer source.
- The same lab user has permission to create pipelines in downstream and read upstream commit metadata.
- Cross-project artifact retrieval is not required; Premium/Ultimate path remains optional.
3. Write predictions before running
| Prediction | Independent verification |
|---|---|
| Upstream trigger creates a separate downstream pipeline in another project | Pipeline graph + downstream pipeline ID/project path |
| Downstream runs source=pipeline at the SHA behind lab-v1 | Downstream evidence file + repository preflight SHA |
| Before allowlisting, downstream job-token request to upstream is denied | Sanitized HTTP 401/403/404 evidence |
| After upstream allowlists downstream, exact upstream commit API request succeeds | HTTP 200 + JSON id equals upstream input SHA |
| strategy: mirror makes trigger status track downstream status | Trigger job status compared with downstream pipeline status |
4. Downstream checkpoint configuration
spec:
inputs:
upstream-project-id:
type: number
upstream-pipeline-id:
type: number
upstream-sha:
type: string
regex: '^[0-9a-f]{40}$'
---
stages: [verify]
verify-platform-contract:
stage: verify
image: alpine:3.20.3
rules:
- if: '$CI_PIPELINE_SOURCE == "pipeline"'
- when: never
before_script:
- apk add --no-cache curl jq
- mkdir -p evidence
script:
- printf 'downstream_project=%s\n' "$CI_PROJECT_ID" > evidence/identity.txt
- printf 'downstream_pipeline=%s\n' "$CI_PIPELINE_ID" >> evidence/identity.txt
- printf 'downstream_ref=%s\n' "$CI_COMMIT_REF_NAME" >> evidence/identity.txt
- printf 'downstream_sha=%s\n' "$CI_COMMIT_SHA" >> evidence/identity.txt
- printf 'source=%s\n' "$CI_PIPELINE_SOURCE" >> evidence/identity.txt
- printf 'upstream_project=%s\n' '$[[ inputs.upstream-project-id ]]' >> evidence/identity.txt
- printf 'upstream_pipeline=%s\n' '$[[ inputs.upstream-pipeline-id ]]' >> evidence/identity.txt
- printf 'upstream_sha=%s\n' '$[[ inputs.upstream-sha ]]' >> evidence/identity.txt
- |
code="$(curl --silent --show-error --output evidence/upstream-commit.json --write-out '%{http_code}' --header "JOB-TOKEN: $CI_JOB_TOKEN" "$CI_API_V4_URL/projects/$[[ inputs.upstream-project-id ]]/repository/commits/$[[ inputs.upstream-sha ]]" )"
printf 'upstream_api_http=%s\n' "$code" >> evidence/identity.txt
test "$code" = "200"
- jq -e --arg sha '$[[ inputs.upstream-sha ]]' '.id == $sha' evidence/upstream-commit.json >/dev/null
- jq -r '{id, short_id, committed_date}' evidence/upstream-commit.json > evidence/upstream-summary.json
artifacts:
name: "ch15-checkpoint-$CI_PIPELINE_ID-$CI_JOB_ID"
expire_in: 1 day
paths:
- evidence/identity.txt
- evidence/upstream-summary.json
For the intentional failure run, keep the same job but expect the API step to fail before the allowlist exists. Preserve the first trace and HTTP status.
5. Upstream checkpoint configuration
stages: [record, orchestrate]
record-upstream:
stage: record
image: alpine:3.20.3
script:
- mkdir -p evidence
- printf 'project=%s\npipeline=%s\nsource=%s\nref=%s\nsha=%s\n' "$CI_PROJECT_ID" "$CI_PIPELINE_ID" "$CI_PIPELINE_SOURCE" "$CI_COMMIT_REF_NAME" "$CI_COMMIT_SHA" > evidence/upstream.txt
artifacts:
name: "ch15-upstream-$CI_PIPELINE_ID-$CI_JOB_ID"
expire_in: 1 day
paths: [evidence/upstream.txt]
orchestrate-downstream:
stage: orchestrate
inherit:
variables: false
trigger:
project: platform-lab/ch15-downstream
branch: lab-v1
strategy: mirror
inputs:
upstream-project-id: $CI_PROJECT_ID
upstream-pipeline-id: $CI_PIPELINE_ID
upstream-sha: $CI_COMMIT_SHA
forward:
yaml_variables: false
pipeline_variables: false
6. Bind and record the downstream ref before triggering
In the disposable downstream repository, review the checkpoint
configuration, then create or update lab-v1 only under
the lab procedure. Record:
DOWNSTREAM_EXPECTED_SHA="$(git rev-parse HEAD)"
printf 'expected_downstream_sha=%s\n' "$DOWNSTREAM_EXPECTED_SHA"
git tag -f lab-v1 "$DOWNSTREAM_EXPECTED_SHA"
git push --force-with-lease origin refs/tags/lab-v1
Safety: force-moving tags is shown only for the disposable lab. Production release tags should normally be protected/immutable by policy, not rewritten. If your lab can create a fresh unique tag instead, prefer that.
7. First run: intentionally preserve authorization failure
Ensure the upstream project does not yet allowlist
downstream for job-token access. Run the upstream pipeline. The
multi-project trigger should create downstream, but downstream's
Commit API request should fail and, because of
strategy: mirror, the trigger/upstream pipeline should
reflect that downstream failure.
Capture upstream pipeline ID/SHA, trigger job ID/status, downstream pipeline ID/ref/SHA/source, failed downstream job ID, and sanitized HTTP status. Do not retry yet.
8. Repair exactly one authorization edge
In the disposable upstream project, add
platform-lab/ch15-downstream to the upstream CI/CD job
token allowlist. Keep project visibility, user roles, refs, YAML,
runner configuration, and variables unchanged. This isolates the
repaired causal edge.
9. Second run: prove exact source and authorization
Run the upstream pipeline again. Verify:
- downstream pipeline source is
pipeline; - downstream ref is
lab-v1; - downstream SHA equals the independently recorded tag SHA;
- the Commit API response HTTP status is 200;
-
the returned upstream commit
idequals the input upstream SHA; - downstream success is mirrored to the trigger job;
- no secret/token value appears in the trace or artifact.
10. Optional second denial: prove the allowlist is narrow
If you own a third disposable project solely for this check, do not add it to the upstream allowlist. A job from that project should not gain the same API access merely because it shares a namespace. If creating a third project is unnecessary, document this as an architecture prediction rather than adding resources.
11. Optional Premium/Ultimate artifact extension
Only after the core checkpoint passes, optionally test
needs:project or CI_JOB_TOKEN artifact API access.
Record the producer project/ref/SHA/job and digest. Document that
this capability is paid-tier and that
needs:project does not wait for an in-progress pipeline
on the ref.
Do not rebuild the upstream artifact in downstream to avoid an authorization or retrieval problem; that would change artifact identity and invalidate the delivery evidence.
12. Required evidence packet
| Evidence item | Required capture |
|---|---|
| Upstream identity | Project path/ID, pipeline ID/source, ref, SHA |
| Trigger contract | Downstream project, requested ref, strategy=mirror, explicit inputs, forwarding disabled |
| Downstream identity | Project path/ID, pipeline ID/source=pipeline, ref, exact SHA |
| Failure baseline | Downstream job ID, first trace, sanitized denied HTTP status, allowlist absent |
| Authorization repair | Target=upstream, source=downstream allowlist entry, user permission assumption |
| Success proof | HTTP 200, returned commit ID equals upstream SHA, downstream job/pipeline success |
| Runner/tool context | Runner/executor/version if observed; Alpine tag and curl/jq versions if material |
| Artifacts | Only synthetic evidence artifacts, producer job IDs, paths, expiry, optional digest |
| Tier assumptions | Core path Free-compatible; cross-project artifact extension Premium/Ultimate |
| Rollback | Remove exact allowlist entry; remove disposable refs/projects; no long-lived token retained |
13. Verification checklist
- Exactly two required disposable projects are involved.
- Requested downstream ref and actual downstream SHA are both recorded.
- No branch/default-ref assumption substitutes for SHA evidence.
- Downstream inputs are explicit and non-secret.
- Variable forwarding is intentionally disabled in the checkpoint.
- The initial API denial is preserved before the allowlist repair.
- The upstream target allowlists only the downstream source needed for the request.
- Allowlisting is not described as granting user permissions.
-
strategy: mirrorbehavior is verified from statuses, not assumed. - No PAT, trigger-token secret, TLS bypass, privileged runner, cloud credential, or production target is introduced.
14. Cleanup and rollback
Remove the exact downstream entry from the upstream job-token
allowlist. Delete only the disposable lab-v1 ref and
lab branches/projects created by the exercise. Export the sanitized
evidence first. Confirm no project visibility or user-role changes
remain and no long-lived token was created.
15. What Chapter 15 adds to the production operating model
You can now orchestrate across repository/project boundaries without losing identity: upstream and downstream refs/SHAs are distinct, pipeline creation and completion are distinct, user permission and job-token allowlisting are distinct, and cross-project artifacts are separate from triggers. Chapter 16 moves to merge-request pipelines, merged-results pipelines, merge trains, and pre-merge validation, where the source revision itself can be synthetic or merge-result-specific and must be reasoned about just as explicitly.
Knowledge check
What single change should separate the failing and successful checkpoint runs?
The upstream target adds the downstream project to its CI_JOB_TOKEN allowlist. Keep refs, YAML, user roles, and runner state unchanged so the causal repair is isolated.
Why does the first downstream failure make the upstream trigger fail in this checkpoint?
The trigger uses strategy: mirror, so the trigger job mirrors the downstream pipeline status instead of reporting creation only.
What proves downstream executed the intended revision?
The downstream CI_COMMIT_SHA must equal the independently recorded SHA behind the governed lab-v1 ref.
Does the successful Commit API request prove artifact access?
No. It proves that supported read-only API access is authorized for that job token/user. Cross-project artifact mechanisms have separate tier and endpoint semantics.
What Chapter 16 identity complication comes next?
Merge-request, merged-results, and merge-train pipelines can execute merge-specific or synthetic revisions, so pre-merge source/SHA identity must be interpreted explicitly.
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
Documentation verification date: 2026-09-11.
Multi-project trigger syntax, downstream inputs/forwarding,
CI_JOB_TOKEN permissions, allowlist controls, artifact APIs, and
needs:project are version-sensitive. Re-check the
GitLab version deployed on Self-Managed or Dedicated instances
before relying on newer behavior.
- Downstream pipelines — multi-project triggers, downstream source values, trigger strategies, inputs, variable forwarding, and cross-project artifact patterns.
- CI/CD job token — token lifetime, user-derived permissions, cross-project allowlists, security, and current allowlist controls.
- Fine-grained job-token permissions — optional endpoint-level restrictions for allowlisted projects/groups.
- Trigger pipelines with the API — trigger tokens, CI_JOB_TOKEN multi-project API triggers, source semantics, and revocation guidance.
-
CI/CD YAML syntax reference
—
trigger,trigger:strategy,trigger:forward,needs:project, and current tier constraints. - Job Artifacts API — artifact download by job/ref and the current tier requirement for cross-project job-token downloads.
CI_PIPELINE_SOURCE=pipeline. By default, a trigger job
succeeds when GitLab successfully creates the downstream pipeline;
strategy: mirror makes the trigger job track downstream
status, while strategy: depend is not recommended for
new designs. Downstream inputs are preferred over broad
variables when defining a configuration contract.
CI_JOB_TOKEN exists only while a job runs; target
projects restrict cross-project token use with an allowlist, and
allowlisting does not grant project membership. Current allowlists
support up to 200 group entries and 200 project entries.
needs:project and CI_JOB_TOKEN-authenticated Job
Artifacts API downloads across projects are Premium/Ultimate
features, so this chapter's mandatory path uses Free-tier
allowlisted API metadata access and treats cross-project artifact
transfer as optional/simulated.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.