8. Synchronizer
Sync CLI 🔁
The Sync CLI manages framework versions, migrations, and releases across all repositories in the dynatrace-wwse organization. One tool to keep every repo up to date.
Overview#
The Sync CLI runs from the codespaces-framework directory and operates on repos listed in repos.yaml. It follows a local-first workflow: repos are cloned locally, changes are made on branches, then pushed as PRs.
Full Release Cycle#
The workflow for updating all repos to a new framework version:
graph LR
A[Release] --> B[Push Update]
B --> C[Monitor PRs]
C --> D[Merge]
D --> E[Tag + Release]
E --> F[Cleanup]
Step 1: Release a new framework version#
Make your changes to the framework, commit them, then release:
This creates a git tag on codespaces-framework, updates the default version in cli.py, and creates a GitHub Release with a categorized changelog (features, fixes, docs, maintenance).
To create a release for an existing tag without bumping:
Step 2: Push updates to all repos#
# Preview what would change
sync push-update --framework-version <version> --dry-run
# Execute: pull main → branch → full migrate → commit → push → PR
sync push-update --framework-version <version>
# --framework-version is optional: omitted, it targets the latest git tag
sync push-update --dry-run
Per repo, push-update:
- 📥 Clones if not present locally
- 🔄 Checks out main and pulls latest
- 🌿 Creates branch
sync/framework-<version> - 🔧 Runs full migration (Category A cleanup, templates, mkdocs, .env, README badges, devcontainer.json fixes)
- 📝 Commits all changes
- 🚀 Pushes branch and creates PR
On failure at any step, the branch is left in place for manual intervention.
To re-push changes at the same version (e.g. after fixing badges or templates):
Step 3: Monitor CI and merge#
# List all open PRs with CI status
sync list-pr
# Filter to sync PRs for a specific version
sync list-pr --framework-version <version>
# Merge passing PRs
sync list-pr --merge
# Close old PRs that are superseded
sync list-pr --framework-version <previous-version> --close -c "Superseded by <version>"
Step 4: Tag consumer repos and create releases#
After all PRs are merged:
# Preview
sync tag --framework-version <version> --bump patch --release --dry-run
# Create combined version tags + GitHub Releases
sync tag --framework-version <version> --bump patch --release
Each repo gets a tag of the form v<framework-version>_<repo-version> and a GitHub Release with categorized changelog.
Step 5: Cleanup#
# Delete merged branches (local + remote)
sync cleanup-branches
# Verify everything is clean
sync validate
Migration: What Happens#
When push-update or migrate runs on a repo, it executes these phases:
| Phase | Action |
|---|---|
| 1. Audit | Identifies Category A files and validates devcontainer.json |
| 1b. Fix devcontainer.json | Moves an exact shinojosa/dt-enablement:v1.0…v1.3.2 image to FRAMEWORK_IMAGE (v1.4.0: v1.3.2's Node 22.23.4 / ENH-075 entrypoint fix + base refresh, libbrotli1 USN-8956-1, docker CLI 28.5.2 — #205). Any other image (custom, scenario, another tag) is left as is. --dry-run reports, writes nothing |
| 2. Clean Category A | Removes framework-owned files (now pulled from cache). Inside .devcontainer/apps, p10k and yaml only a path the framework ships or has ever shipped is removed, plus generated output (gen/, *-gen.yaml); a repo's own files there (e.g. yaml/dt-tokens.yaml, yaml/dynakube-config.yaml, yaml/otel-collector.yaml) are kept (1.19.1) |
| 3. Thin Makefile | Installs wrapper that delegates to cached makefile.sh |
| 4. source_framework.sh | Installs/updates versioned pull mechanism |
| 5. mkdocs.yaml | Converts to INHERIT: mkdocs-base.yaml pattern |
| 6. overrides/main.html | Parameterizes RUM snippet, extracts URL to mkdocs.yaml |
| 7. deploy-ghpages.yaml | CI fetches mkdocs-base.yaml at framework version |
| 8. .gitignore | Adds cache, mkdocs-base, generated-file (**/gen/**.yaml, **-gen.yaml), Dockerfile.framework entries |
| 9. .env location | Moves from runlocal/.env to .devcontainer/.env |
| 10. README badges | Updates branding, adds missing badges, fixes integration test link |
All Commands#
Run sync <command> --help for the authoritative flags — the table below lists what each command is
for, not every option it takes.
Migration & Updates#
| Command | Description | Example |
|---|---|---|
push-update |
Full workflow: pull → branch → migrate → push → PR | sync push-update --framework-version <version> |
diff |
Preview what push-update would change, without touching git |
sync diff |
migrate |
Migrate repos to the versioned pull model | sync migrate --dry-run |
migrate-mkdocs |
Convert a single repo's mkdocs.yaml to the INHERIT pattern |
sync migrate-mkdocs --repo <owner/name> |
revert |
Undo uncommitted changes | sync revert --repo template |
validate |
Check repos.yaml schema and local repo state, including docs structure and the training's delivery front matter |
sync validate |
check-delivery |
Check one training checkout's .devcontainer/delivery.yaml or delivery front matter; exit 1 when invalid (see Training delivery) |
sync check-delivery path/to/repo |
Versioning & Releases#
| Command | Description | Example |
|---|---|---|
release |
Tag framework, create GitHub Release | sync release --part patch |
tag |
Tag consumer repos with the combined version | sync tag --framework-version <version> --bump patch --release |
bump-repo-version |
Bump only a repo's own version component | sync bump-repo-version --repo <owner/name> --part minor |
Repository Management#
| Command | Description | Example |
|---|---|---|
clone |
Clone repos from repos.yaml |
sync clone --all |
checkout |
Check out main across repos and show status |
sync checkout --pull |
protect-main |
Enable branch protection on the default branch | sync protect-main --dry-run |
cleanup-branches |
Delete merged branches (local + remote) | sync cleanup-branches --dry-run |
Monitoring#
| Command | Description | Example |
|---|---|---|
list-pr |
List PRs, approve, merge, or close | sync list-pr --merge |
list-issues |
List open issues | sync list-issues --label bug |
ci-status |
Latest CI run status across repos | sync ci-status |
status |
Show version drift | sync status |
list |
List registered repos | sync list --sync-managed --json |
Registry generation#
| Command | Description | Example |
|---|---|---|
generate-registry |
Build the registry consumed by The Hub | sync generate-registry |
generate-json |
Emit repos.json for the published catalog |
sync generate-json |
protect-main resolves the branch and the required check
It reads the default branch from the GitHub API rather than assuming main, accepts repos that
are not in repos.yaml via --repo, and takes --check (repeatable) to set which status-check
contexts are required. The default context is the integration-test job name — see
Testing.
Common Recipes#
First-time setup#
sync clone # Clone all repos locally
sync validate # Check current state
sync migrate --dry-run # Preview migration
Deploy a framework update#
sync release --part patch # bump + tag + GitHub Release
sync push-update --framework-version <version> # Create PRs
sync list-pr --framework-version <version> # Monitor CI
sync list-pr --merge # Merge passing
sync tag --framework-version <version> --bump patch --release # Tag + release
sync cleanup-branches # Clean up
Fix something and re-push at the same version#
Close old PRs#
Housekeeping#
sync protect-main # Protect main on all repos
sync cleanup-branches # Delete stale branches
sync list-issues # Check for open issues
Key Files#
| File | Purpose |
|---|---|
sync/cli.py |
CLI entry point |
sync/core/repos.py |
repos.yaml parsing, RepoEntry dataclass |
sync/core/version.py |
Version parsing and bumping |
sync/core/github_api.py |
GitHub API via gh CLI |
sync/core/local_git.py |
Local git operations |
sync/commands/migrate.py |
Migration logic, file classification, templates |
sync/commands/push_update.py |
Local-first push-update |
sync/commands/release.py |
Framework release with changelog |
sync/commands/tag.py |
Consumer repo tagging + releases |
sync/commands/list_pr.py |
PR listing, approve, merge, close |
repos.yaml |
Registry of all repos |
sync/tests/ |
The CLI's own pytest suite — see Testing |
Run the tests before changing sync/ or repos.yaml
The suite covers repos.yaml parsing, version maths and every subcommand, and it needs no
network — but no GitHub Actions workflow runs it, so a regression only shows up if you run it:
Command Scope#
| Command | Repos it operates on |
|---|---|
push-update, migrate, revert, tag |
sync-managed only (consumer repos) |
list-pr, list-issues, protect-main, cleanup-branches |
all active repos (including framework, website, tracker) |
clone |
sync-managed by default, --all for everything |
release |
framework repo only |
Training delivery#
A training declares how it may be delivered in the front matter of its docs/index.md
(the docs_dir from mkdocs.yaml, if set), next to the catalog fields description,
tags, difficulty and duration:
---
description: "Build your own observability agent"
tags: [interactive]
delivery:
availability: workshop-only # self-service (default) | workshop-only
tenancy: single # any (default) | single | multi
---
| Field | Values | Meaning |
|---|---|---|
availability |
self-service (default), workshop-only |
self-service can be started by a learner on their own; workshop-only only inside a workshop |
tenancy |
any (default), single, multi |
how a workshop seats learners: all on the workshop's tenant (single), each on their own (multi), or the trainer chooses (any) |
No block at all is valid and means self-service, tenancy any — what every training
did before this field existed. A block that is present but wrong (an unknown key, a
misspelt value) is refused, because silently falling back to the default would make the
training self-service.
Shared infrastructure is not declared. A training has it when it ships
.devcontainer/workshop-post-create.sh. Such a training must declare
availability: workshop-only and tenancy: single — multi-tenant shared
infrastructure is not supported yet. A workshop-post-start.sh without the create script
is refused too.
sync check-delivery and sync validate enforce these rules. Orbital applies the same
rules at runtime (shared/delivery.py mirrors sync/core/delivery.py, case for case).
.devcontainer/delivery.yaml (ENH-163)#
One file, next to the workshop scripts, that says how the training is delivered. When it exists it wins over the index.md block.
version: 1 # required
type: same-tenant # same-tenant | multi-tenant (tenancy single | multi)
availability: workshop-only # workshop-only | self-service (default self-service)
seats:
personal: false # learners get a personal environment
# (default true; false for a shared training)
mode: full # full | dtctl-only (default full)
shared: # only with .devcontainer/workshop-post-create.sh
instance: m6a.4xlarge # the shared machine's shape (default: Orbital's setting)
- Unknown keys and bad values are refused, as for the block.
- With
workshop-post-create.shthe file must saytype: same-tenantandavailability: workshop-only;seats.personal: trueis refused until learners can have a seat beside a shared machine. Ashared:block without the script is refused. - Both the file and an index.md block: disagreeing on availability or tenancy is refused (two truths); agreeing passes with a warning to remove the block.
shared.instanceis checked by Orbital at booking against its allowlist (workshop_shared_instance_allowlist, defaultm6a.2xlarge, m6a.4xlarge, r6a.2xlarge, r6a.4xlarge), its ceiling (workshop_shared_instance_max_mb, default 131072 MiB) and its capacity table. A shape outside them refuses the booking withshared_instance_not_allowed— never a silent fallback. The lint here checks only the shape's form; the allowlist is Orbital's, read when the workshop is booked.
Moving every training to the file, and later refusing the index.md block, are separate steps.