Chapter 18Lesson 02~300 minutes

Manual Jobs, Delayed Jobs, Scheduled Pipelines, Rollbacks, Freeze Windows, and Release Controls: Guided Hands-On Workflow and Core Operations

Operate a disposable release-control pipeline with explicit manual behavior, a short delayed job, schedule ownership, freeze policy, stale-path handling, and known-artifact recovery.

Hands-onManual gatestart_inSchedule APISynthetic deploymentCleanup

Learning objectives

  • Inspect current release-control state before modifying CI/CD or schedules.
  • Run safe optional and blocking manual jobs and prove their different pipeline semantics.
  • Observe a short delayed job, unschedule/cancel it safely, and correlate timestamps.
  • Create and inspect an inactive pipeline schedule without recurring side effects.
  • Model freeze behavior and known-artifact recovery with synthetic deployment state.
Availability baseline (verified 2026-08-22 against GitLab 19.3 current documentation). Manual jobs, manual_confirmation, delayed jobs with start_in, pipeline schedules, deployment freeze periods / CI_DEPLOY_FREEZE, environment/deployment history, the project setting that prevents outdated deployment jobs, and rollback/retry controls are available on GitLab Free/Premium/Ultimate across GitLab.com, Self-Managed, and Dedicated. Protected environments and deployment approvals—the fine-grained “only these people may deploy” layer—remain Premium/Ultimate. The mandatory chapter path uses only a disposable project, synthetic artifacts/environments, and tiny jobs. No production endpoint, cloud account, paid tier, runner registration, real credential, or release publication is required.

1. Scenario: release control with zero external side effect

Use a disposable GitLab Free project or branch named ch18/release-control-lab. The workflow creates one tiny text artifact and “deploys” it only by validating its checksum and recording a synthetic GitLab environment such as release-control/ch18. The external URL is example.invalid. No registry, cloud, Kubernetes cluster, package publication, production tag, or real secret is used.

Preflight Required state Why
Project Disposable or dedicated lab project. You will add CI config and optionally an inactive schedule/freeze period.
Role Developer for normal pipeline/manual work; Maintainer/Owner only for schedule/freeze/project-setting operations that require it. Least privilege.
Runner Tiny hosted/project runner if available; otherwise CI Lint + supplied expected-state fixtures. No paid compute dependency.
Environment Synthetic only: release-control/ch18. No external state can be harmed.
Secrets None. This chapter does not test secret handling.

2. Inspect before-state and record ownership

PROJECT_ID="12345678"

glab api "projects/$PROJECT_ID" --jq '{id,path_with_namespace,default_branch,ci_forward_deployment_enabled,ci_forward_deployment_rollback_allowed}'
glab api "projects/$PROJECT_ID/pipeline_schedules" --paginate \
  --jq '.[] | {id,description,ref,cron,cron_timezone,active,owner:.owner.username}'
glab api "projects/$PROJECT_ID/freeze_periods" --paginate \
  --jq '.[] | {id,freeze_start,freeze_end,cron_timezone}'
glab api "projects/$PROJECT_ID/environments?name=release-control/ch18" \
  --jq '.[] | {id,name,state,external_url,last_deployment:(.last_deployment.id // null)}'

If an existing schedule/freeze/environment belongs to someone else, do not repurpose it. Use uniquely named disposable resources and record their IDs for targeted cleanup.

3. Add a deterministic release artifact and three control jobs

stages: [build, gate, deploy]

build_release:
  stage: build
  script:
    - mkdir -p out
    - printf 'commit=%s\npipeline=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" > out/release.txt
    - sha256sum out/release.txt > out/release.txt.sha256
  artifacts:
    paths: [out/release.txt, out/release.txt.sha256]
    expire_in: 2 days
    access: developer

optional_preview:
  stage: gate
  needs: [build_release]
  script:
    - echo "optional_preview=true"
  when: manual
  allow_failure: true
  manual_confirmation: "Run optional synthetic preview?"

release_gate:
  stage: gate
  needs: [build_release]
  script:
    - sha256sum -c out/release.txt.sha256
    - echo "release_gate=approved_by_manual_start"
  rules:
    - if: '$CI_COMMIT_BRANCH'
      when: manual
      allow_failure: false

wait_before_deploy:
  stage: deploy
  needs:
    - job: build_release
      artifacts: true
    - job: release_gate
      artifacts: false
  script:
    - sha256sum -c out/release.txt.sha256
    - echo "delayed_window_complete=true"
  when: delayed
  start_in: 20 seconds
  environment:
    name: release-control/ch18
    url: https://ch18.example.invalid
  resource_group: ch18-release

4. Validate before pushing

Use Pipeline Editor / CI Lint. The expected merged semantics are:

  • optional_preview may be ignored without blocking the pipeline.
  • release_gate is blocking because allow_failure: false is explicit.
  • wait_before_deploy does not begin its delay until the gate succeeds and the deploy stage becomes eligible.
  • The deploy job verifies the exact artifact checksum; it does not rebuild anything.
  • resource_group prevents concurrent mutation of this synthetic environment.

5. Commit to a disposable branch

git switch -c ch18/release-control-lab
git add .gitlab-ci.yml
git diff --cached --check
git diff --cached
git commit -m "ch18: release control lab"
LAB_SHA="$(git rev-parse HEAD)"
printf 'lab_sha=%s\n' "$LAB_SHA"
git push -u origin ch18/release-control-lab

6. Observe manual-state differences before clicking anything

Inspect the pipeline. Record pipeline ID/SHA/status and job statuses. Predict that optional_preview is skipped/optional, while release_gate leaves the pipeline blocked. Verify from the jobs API:

PIPELINE_ID="123456789"
glab api "projects/$PROJECT_ID/pipelines/$PIPELINE_ID" --jq '{id,sha,ref,source,status}'
glab api "projects/$PROJECT_ID/pipelines/$PIPELINE_ID/jobs?scope[]=manual&scope[]=skipped" --paginate \
  --jq '.[] | {id,name,status,allow_failure,stage,ref}'

7. Run the blocking gate deliberately

Use the GitLab UI to open release_gate, confirm it is tied to LAB_SHA, then run it. The actor must have permission to merge to the assigned branch. Do not pass any secret variables. Record the job ID and user shown in the job/deployment evidence.

After the gate succeeds, verify the delayed deploy is scheduled rather than pending/running immediately.

8. Observe the short delay and exercise Unschedule

For the first run, let the 20-second delay complete and record created_at, started_at, and finished_at. The runner may add queue time, so do not expect the observed wall-clock difference to equal exactly 20 seconds.

For a second disposable pipeline, trigger the gate, then select Unschedule on the delayed job. Verify the automatic timer is canceled. You can leave it manual or deliberately run it after confirming the SHA; do not hide the state change.

glab api "projects/$PROJECT_ID/pipelines/$PIPELINE_ID/jobs" --paginate \
  --jq '.[] | select(.name=="wait_before_deploy") | {id,name,status,created_at,started_at,finished_at,sha}'

9. Create an inactive schedule as a safe server-side object

Preferred UI path: Build → Pipeline schedules → New schedule. Use a clear description, a harmless cron such as daily, the disposable branch, UTC, and leave it inactive if your UI/API supports that path. Do not add credentials.

API alternative for a disposable project:

SCHEDULE_JSON="$(glab api --method POST "projects/$PROJECT_ID/pipeline_schedules" \
  --field description='ch18 disposable schedule' \
  --field ref='ch18/release-control-lab' \
  --field cron='17 3 * * *' \
  --field cron_timezone='UTC' \
  --field active='false')"
SCHEDULE_ID="$(printf '%s' "$SCHEDULE_JSON" | python -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
printf 'schedule_id=%s\n' "$SCHEDULE_ID"

glab api "projects/$PROJECT_ID/pipeline_schedules/$SCHEDULE_ID" \
  --jq '{id,description,ref,cron,cron_timezone,active,owner:.owner.username,next_run_at}'

The schedule should not create recurring pipelines while inactive. If your GitLab version/API rejects inactive creation, create it in the UI, immediately deactivate it, and verify no pipeline ran before proceeding.

10. Prove source/identity without consuming a recurring schedule

A genuine scheduled pipeline has CI_PIPELINE_SOURCE=schedule. You may use the schedule’s one-time Run action if you want a tiny pipeline and have quota; remember that manual Run uses your permissions instead of the stored owner. The no-compute path uses this expected fixture:

{
  "source": "schedule",
  "owner": "schedule-owner",
  "target_ref": "refs/heads/ch18/release-control-lab",
  "active": false,
  "policy": "no recurring execution during lab"
}

11. Model a deploy freeze safely

Because freeze periods are Free, a Maintainer can create one in a disposable project. However, cron time windows are easy to miscalculate. The mandatory lab can stay read-only: inspect the current freeze list and evaluate a fixture where CI_DEPLOY_FREEZE=true. Optional live exercise: add a short, clearly named freeze period that includes the current time, validate the time zone carefully, create one tiny pipeline, then remove the freeze period immediately.

Use this policy pattern:

deploy_release:
  stage: deploy
  script:
    - sha256sum -c out/release.txt.sha256
    - echo "synthetic_deploy=true"
  rules:
    - if: '$CI_DEPLOY_FREEZE'
      when: manual
      allow_failure: true
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: manual
      allow_failure: false
    - when: never

Prediction: during a freeze the routine release does not auto-run. It becomes an explicit exception path. Whether an actor is allowed to take that exception is a separate authorization question.

12. Simulate rollback with immutable identity

Record the checksum from the successful deployment. Create a deliberate failed synthetic deployment job that checks the same artifact and exits non-zero after printing only a synthetic reason. Preserve the failure. Recovery must deploy the previously known checksum/commit, not rerun a build against mutable dependencies.

simulate_release_failure:
  stage: deploy
  needs:
    - job: build_release
      artifacts: true
  script:
    - sha256sum -c out/release.txt.sha256
    - echo "ERROR: synthetic release rejected" >&2
    - exit 42
  environment:
    name: release-control/ch18
    url: https://ch18.example.invalid
  resource_group: ch18-release
  when: manual
  allow_failure: false

For recovery, run a new safe deployment job/pipeline tied to the known commit/artifact. If you inspect GitLab’s retry/rollback UI, treat it as a mechanism whose behavior must preserve the intended identity—not as proof by itself.

13. Challenge: choose the right clock

You need a job to run every night even if nobody pushes. Should you use start_in: 24 hours or a pipeline schedule? Use a pipeline schedule: the requirement is recurring pipeline creation independent of the lifetime of any one pipeline. start_in is for a delay inside an existing pipeline and is capped at one week.

14. Cleanup all server-side and Git refs you created

Delete only the schedule whose captured ID is SCHEDULE_ID:

# Verify before delete.
glab api "projects/$PROJECT_ID/pipeline_schedules/$SCHEDULE_ID" --jq '{id,description,ref,active}'
# Destructive but targeted: deletes the disposable schedule object only.
glab api --method DELETE "projects/$PROJECT_ID/pipeline_schedules/$SCHEDULE_ID"

# Verify it is gone; a 404 is expected after deletion.
# Then stop the synthetic environment in the UI if still available.

git switch main
git ls-remote --heads origin refs/heads/ch18/release-control-lab
# Delete only after the exact ref is confirmed:
git push origin --delete ch18/release-control-lab
git branch -D ch18/release-control-lab

If you created a live freeze period, verify its exact ID/cron/time zone before deleting it via UI/API. Never “clean up all freeze periods.”

15. Evidence bundle

  • Pipeline ID + SHA + source.
  • Manual gate job ID/status/actor and explicit allow_failure behavior.
  • Delayed job timestamps plus whether it completed or was unscheduled.
  • Schedule ID/owner/ref/cron/active state, with no credentials.
  • Freeze-policy fixture or exact disposable freeze-period ID if used.
  • Artifact checksum and deployment history before failure and after recovery.
  • Cleanup proof for schedule/freeze/ref/environment.

Knowledge check

Why is the schedule created inactive in the mandatory path?

What proves the blocking manual gate applies to the intended code?

What does Unschedule do to a delayed job?

Why do you record the schedule owner?

Why is the synthetic failure not hidden with allow_failure: true?

Summary

You created the release-control objects without production risk: optional and blocking manual jobs, a delayed job, an inactive schedule, freeze-policy behavior, one controlled failure, and a known-artifact recovery. The key evidence is always actor/time/source/SHA/artifact/environment—not the color of a single button.

Official references

Next lesson

Choose the least fragile release-control architecture

Lesson 3 compares human gates, approvals, schedules, delays, freezes, stale-deployment controls, and emergency exceptions by maintainability, security, reliability, cost, and auditability.

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.