Audience: Operators and contributors who need the exact package layout and file-scope semantics.
PackageRoot is the configured package working directory. Project artefacts always live beneath organisation and project folders inside that root:
<WorkingDirectory>/
.migration/
<org-folder-name>/<project>/
For example, given:
WorkingDirectory:storage\my-export- Organisation URL:
https://dev.azure.com/contoso - Project:
MyProject
The resulting project subtree is storage\my-export\contoso\MyProject\.
For Azure DevOps Services, the org folder name is the last path segment of the organisation URL, for example contoso from https://dev.azure.com/contoso. For TFS collection URLs such as http://tfs:8080/tfs/DefaultCollection, the folder name is DefaultCollection.
The package has three state scopes:
- Root
.migration/is authoritative package-wide orchestration state shared across runs. /{org}/{project}/.migration/is authoritative project-scoped resume state..migration/runs/<runId>/is run-scoped audit output only.- Cursor identity is action-qualified per module (
<action>.<module>.cursor.json) to prevent cross-phase collisions.
Run-scoped job.json, plan.json, and config.json are copies of what was executed for that run. They are not the source of truth for later resume, phase-gate, or orchestration decisions.
PackageRoot/
.migration/
migration-config.json
plan.json
inventory.complete.json
prepare.complete.json
Checkpoints/
idmap.db
export_progress.db
runs/
<runId>/
job.json
plan.json
config.json
logs/
progress.ndjson
diagnostics.ndjson
diagnostics-001.ndjson
{org}/{project}/
manifest.json
WorkItems/
Nodes/
referenced-paths.json
source-tree.json
prepare-report.json
Teams/
{team-slug}/
team.json
prepare-report.json
Permissions/
prepare-report.json
Builds/
Git/
Identities/
descriptors.jsonl
mapping.json
unresolved.json
prepare-report.json
.migration/
inventory.workitems.cursor.json
export.workitems.cursor.json
import.workitems.cursor.json
export.identities.cursor.json
Root .migration/ is the only valid location for package-wide state shared across runs, including:
- resolved package configuration
- execution plan
- phase completion markers
- package-wide checkpoint databases
- run audit folders
Each project-local .migration/ folder stores cursors for one organisation, project, action, and module combination. Filenames follow this shape:
<action>.<module>.cursor.json
Examples:
inventory.workitems.cursor.jsonexport.workitems.cursor.jsonimport.workitems.cursor.jsonexport.identities.cursor.json
Each run folder is an audit snapshot for one execution. It contains:
job.json— audit copy of the leased jobplan.json— audit copy of the plan executed for that runconfig.json— audit copy of the resolved configuration used by that runlogs/— structured progress and diagnostic records for that run
Run folders use UTC timestamp format <yyyyMMdd-HHmmss> so they sort chronologically.
The WorkItems layout is canonical and must preserve lexicographic chronological ordering:
WorkItems/
yyyy-MM-dd/
<ticks>-<workItemId>-<revisionIndex>/
revision.json
[comment.json]
<attachment files>
<embedded image files>
<ticks>-<workItemId>-c<commentId>/
comment.json
<embedded image files>
Key characteristics:
- chronological ordering is guaranteed across revision and comment sub-folders
- no global index is required
- streaming import can enumerate in lexicographic order without resorting in memory
- attachments and embedded images stay beside the data that references them
- each folder is a natural resume position
manifest.json is project-scoped package metadata.
{
"packageVersion": "1.0",
"toolVersion": "x.y.z",
"runId": "...",
"configHash": "...",
"source": {
"type": "AzureDevOpsServices | TeamFoundationServer",
"orgOrCollection": "...",
"project": "...",
"apiVersion": "..."
},
"includedTypes": [
"WorkItems",
"Teams",
"Identities",
"Nodes",
"Permissions",
"Builds",
"Git"
],
"schemaVersions": {
"WorkItems": "1.0",
"Teams": "1.0"
}
}The manifest is not required for streaming import, but it is required for validation, compatibility checks, upgrade safety, and portable tooling.
Packages created before the split between root .migration/ and project-local .migration/ may store cursor files under root .migration/Checkpoints/ or legacy Checkpoints/. Readers should try the project-local location first, then fall back to the legacy root-level locations.
Packages created before run-scoped logging may have diagnostic files directly under .migration/Logs/. Tooling may fall back to that flat layout when no run-scoped folder is present.
The package layout and the package boundary are related but not identical concerns.
- This file owns the exact layout, file names, and scope semantics.
- package-boundary-reference.md owns the contributor-facing description of
IPackageAccess, routing ownership, and the relationship between the boundary and the underlying persistence stores.
Zip is a transport wrapper around the package directory layout. It does not redefine the package format.
PackageRoot/ -> migration-package.zip
- pack is applied after export completes or on demand via tooling
- the zip preserves the relative directory structure from
PackageRoot/downward - no content transformation occurs during packing
migration-package.zip -> PackageRoot/
- unpack extracts to a specified directory before import begins
- if
artefacts.zipistruein configuration, unpack is handled automatically by the runner before import - partial extraction of root
.migration/, a project.migration/, or.migration/runs/<runId>/is a valid tooling scenario
- order preservation: unpacking preserves traversal order
- streaming import compatibility: unpacked packages behave the same as in-place packages
- determinism: identical export inputs produce structurally identical packages, excluding run-specific audit data and timestamped metadata
- use zip64 for packages larger than standard zip limits
- pack and unpack tooling must enable zip64 when needed
- full extraction is not required by the core package design