A lightweight, Git-style workflow for TFVC modules: install, restore, update, remove — deterministic and CI-friendly.
A PowerShell script that treats TFVC content like a small dependency graph. You declare TFVC modules in a JSON manifest, then run simple commands to map/get specific versions, lock them for reproducibility, and tear everything down cleanly in CI.
- Four commands:
install,restore,update,remove(+help). - Deterministic: maintains a lockfile with the exact
resolvedChangesetfor each module. - Version flexibility: supports
C123, plain numbers123, labelsLmylabel/label, datesDyyyy-mm-dd, andT(tip). - Multi-collection: modules may come from different TFVC collections in one run.
- CI-friendly: non-interactive (
/noprompt), creates ephemeral workspaces in CI and removes them after the run. - Predictable layout: all local mappings live under a single
defaultWorkspaceroot, separated by collection.
- Windows with PowerShell 5+ or PowerShell 7+.
- tf.exe available on PATH (e.g., Visual Studio / Team Explorer, Azure DevOps Server client tools). Run
tf /?to verify. - Access to your TFVC collection(s) and paths with sufficient permissions.
No module packaging required. Keep the script in your repo (e.g., ./tools/TfvcPackageManager.ps1).
Typical invocation:
# From your repo root
pwsh ./tools/TfvcPackageManager.ps1 -Command installYou can also use Windows PowerShell:
powershell ./tools/TfvcPackageManager.ps1 -Command installStart with a minimal manifest that declares your TFVC collection(s), workspace root, and modules. Example:
{
"defaultCollection": "http://desktop-6o5gjpr:8086/DefaultCollection",
"defaultWorkspace": "E:\\Development\\package-manager-tf\\workspaces",
"modules": [
{
"name": "common-file",
"collection": "http://desktop-6o5gjpr:8086/Common",
"serverPath": "$/common-code/common-file.md",
"localPath": "common\\common-file.md",
"version": "T"
},
{
"name": "another-common-file",
"collection": "http://desktop-6o5gjpr:8086/Common",
"serverPath": "$/common-code/another-common-file.md",
"localPath": "common\\another-common-file.md",
"label": "Release-7.0"
},
{
"name": "main-repo",
"serverPath": "$/main-repo",
"localPath": "main-repo",
"version": "T"
}
]
}# 1) Install (map & get) everything defined in the manifest
pwsh ./tools/TfvcPackageManager.ps1 -Command install
# 2) Update one module to a new version/label in the manifest, then refresh lockfile
pwsh ./tools/TfvcPackageManager.ps1 -Command update -Name common-file
# 3) Restore all modules to the exact changesets pinned in the lockfile
pwsh ./tools/TfvcPackageManager.ps1 -Command restore
# 4) Remove mappings and delete local folders (careful!)
pwsh ./tools/TfvcPackageManager.ps1 -Command removeTip: Add
-Mode CIto force CI behavior locally (ephemeral workspaces), or leave-Mode Auto(default) and it will detect CI by environment variables.
The manifest drives everything. Place it at ./config-specification.json (default) or pass -ManifestPath.
defaultCollection(string, required): Fallback collection URL used when a module doesn’t specifycollection.defaultWorkspace(string, required): Absolute folder where all TF operations and local mappings live. The script executestffrom here and places per-collection subfolders beneath it.
Each entry describes one TFVC-backed unit you want on disk.
name(string, required): Friendly identifier (unique within the manifest). Used for filtering via-Name.collection(string, optional): Collection URL for this module. If omitted,defaultCollectionis used.serverPath(string, required): TFVC path (e.g.,$/Project/Folderor a single file).localPath(string, required): Relative path under the module’s collection folder insidedefaultWorkspace. You can point to a folder (recommended) or a specific file path if you only need one file.version(string/number, optional): Target version. If numeric123, it becomesC123. You can also provideC123,T,D2025-09-01, etc.label(string, optional): A label to resolve ifversionis not provided.
Version priority:
version→label→T(tip/latest).
For each module, the script computes the final local mapping like this:
- Determine the collection name: the last segment of the collection URL (e.g.,
http://host:8086/Common→Common;.../DefaultCollection→DefaultCollection). If the module has nocollection, the script usesdefaultCollection. - Build the workspace working folder:
JoinPath(defaultWorkspace, <collectionName>). Alltfcommands for that module run from this folder. - Build the module local mapping:
JoinPath(defaultWorkspace, <collectionName>, module.localPath). - Ensure the local directories exist and then map:
tf workfold /map "$serverPath" "$localLocalPath"in the computed workspace.
This keeps modules from different collections neatly separated under the same defaultWorkspace root:
<defaultWorkspace>/
DefaultCollection/
main-repo/ <-- module.localPath
Common/
common/ <-- module.localPath
common-file.md
common/
another-common-file.md
- If
versionis numeric (e.g.,123), it is normalized toC123. - If
versionalready includes a prefix (C,L,D,T), it is used as-is. - If
versionis omitted butlabelis set, it resolvesL<label>. - If both are omitted, the script uses
T(latest).
The script always resolves your spec (label/date/T) to a concrete changeset number and saves it to the lockfile.
All commands are passed via -Command <Name>. Commands are case-insensitive.
-
-ManifestPath(string): Path to your manifest file. Default:./config-specification.json. -
-LockPath(string): Path to the lockfile. Default:./config-specification.lock.json. -
-Name(string): Process only the module with this name. -
-Mode(Auto | Local | CI):- Auto (default): Detect CI via environment (e.g.,
TF_BUILD,GITHUB_ACTIONS,CI). If found →CI, elseLocal. - Local: Uses a persistent workspace per collection.
- CI: Creates ephemeral workspaces and removes them after the command completes.
- Auto (default): Detect CI via environment (e.g.,
Tip: You can run
-Mode CIlocally to simulate CI behavior and verify reproducibility.
Purpose: Map TFVC paths to your local filesystem and tf get to the desired versions. Also writes/updates the lockfile with the exact resolvedChangeset for each module.
Use when:
- First time setting up a repo.
- Adding a new module to the manifest.
- Switching machines and you want to populate dependencies.
Examples:
# Install all modules
pwsh ./tools/TfvcPackageManager.ps1 -Command install
# Install only one module by name
pwsh ./tools/TfvcPackageManager.ps1 -Command install -Name common-file
# Force CI mode locally (ephemeral workspaces)
pwsh ./tools/TfvcPackageManager.ps1 -Command install -Mode CIWhat it does:
- For each module (filtered by
-Nameif provided), resolve its collection (module.collection or defaultCollection). - Create/confirm a workspace and local mapping.
- Determine version spec (from
version/labelwith the priority rules) and runtf get. - Resolve the actual changeset ID and write/update the lockfile.
- If in
CImode, remove the ephemeral workspace(s) after completion.
Purpose: Make your working copy match the lockfile exactly. Reads each module’s resolvedChangeset and runs tf get /version:C<changeset>.
Use when:
- Reproducing a previous build or state.
- CI pipelines that require deterministic inputs.
- Onboarding a teammate to the exact versions your build expects.
Examples:
# Restore everything pinned in the lockfile
pwsh ./tools/TfvcPackageManager.ps1 -Command restore
# Restore only a single module by name
pwsh ./tools/TfvcPackageManager.ps1 -Command restore -Name main-repoWhat it does:
- Reads the lockfile.
- For each entry, confirms workspace/mapping.
- Runs
tf getto the exact pinned changeset. - Optionally tears down CI workspaces.
Purpose: Re-evaluate versions/labels in the manifest, sync the working copy, and refresh the lockfile with the newly resolved changesets.
Use when:
- You’ve updated
versionorlabelfor one or more modules in the manifest. - You want to advance a module to a new tip or label and lock it.
Examples:
# Update all modules per the manifest and refresh lockfile
pwsh ./tools/TfvcPackageManager.ps1 -Command update
# Update a single module
pwsh ./tools/TfvcPackageManager.ps1 -Command update -Name another-common-fileWhat it does:
- For each module, compute the version spec from the manifest.
- Run
tf getto that spec. - Resolve the concrete changeset and upsert the lockfile entry.
- Optionally tear down CI workspaces.
Purpose: Unmap server↔local mappings and delete local directories. In CI, also removes transient workspaces.
Use when:
- Cleaning a machine or pipeline workspace.
- Decommissioning a module.
- Ensuring no stale mappings remain.
Examples:
# Remove all mappings and delete local folders
pwsh ./tools/TfvcPackageManager.ps1 -Command remove
# Remove only one module
pwsh ./tools/TfvcPackageManager.ps1 -Command remove -Name common-fileWhat it does:
- Confirms the workspace.
- Unmaps the server path.
- Deletes the corresponding local directory.
- In CI, removes ephemeral workspaces.
⚠️ Destructive: This deletes local directories underdefaultWorkspace/<collectionName>/<localPath>.
Prints a concise usage guide and version examples.
pwsh ./tools/TfvcPackageManager.ps1 -Command helpThe script creates one workspace per collection for the current run. Names are deterministic and differ between Local and CI (CI may include build IDs). This avoids collisions and makes cleanup straightforward.
- Maps are created with
tf workfold /mapto the computed local path beneathdefaultWorkspace/<collectionName>. - Syncs happen with
tf getfrom the collection working folder.
Written to ./config-specification.lock.json by default. For each module it records the exact resolvedChangeset and associated metadata needed to restore deterministically.
Example (simplified):
{
"modules": [
{
"name": "common-file",
"collection": "http://desktop-6o5gjpr:8086/Common",
"serverPath": "$/common-code/common-file.md",
"localPath": "common\\common-file.md",
"resolvedChangeset": 1234
}
]
}When running in CI mode (either auto-detected or forced), the script removes any workspaces it created, leaving the runner clean for the next job.
- Deterministic builds: Commit the lockfile; pipelines run
restorefor pinned, reproducible inputs. - Advancing dependencies: Update
versionor switchlabelin the manifest; runupdateto sync and refresh the lock. - Multi-collection estates: Pull modules from several TFVC collections into one predictable local tree under
defaultWorkspace.
Azure DevOps (PowerShell@2):
- task: PowerShell@2
displayName: Install TFVC modules
inputs:
pwsh: true
filePath: ./tools/TfvcPackageManager.ps1
arguments: -Command install -Mode CI -ManifestPath ./config-specification.json -LockPath ./config-specification.lock.jsonGitHub Actions:
- name: Install TFVC modules
shell: pwsh
run: |
./tools/TfvcPackageManager.ps1 -Command install -Mode CI \
-ManifestPath ./config-specification.json \
-LockPath ./config-specification.lock.jsonEnsure
tf.exeis available on the runner (e.g., pre-installed on your self-hosted agent or via a tool step).
tfnot recognized: Install Visual Studio / Team Explorer or Azure DevOps Server client tools; ensuretf.exeis on PATH.- “Path is already mapped”: A previous workspace holds that mapping. Run
remove(CI) or manually unmap/clean the workspace viatf workfold /unmapandtf workspace /deleteas needed. $tffolder appears: TFVC client metadata. It’s safe to delete when you are done with that workspace, but prefer usingremoveto keep state consistent.- Permissions: Verify your account can read the specified
serverPaths. - Locked files: Use
/overwrite(the script already does) and ensure no external tool has the files open.
Q: Can I mix version and label?
A: Yes, but if both are present, version wins. Use one per module to be explicit.
Q: Can I specify a date?
A: Yes. Use Dyyyy-mm-dd (e.g., D2025-09-01). The script resolves it to the latest changeset on/after that date.
Q: Does the script modify TFVC history? A: No. It only maps folders and syncs content to your local machine.
Q: Linux/Mac support?
A: The workflow relies on tf.exe (Windows). Use Windows agents for CI.
Q: Can localPath point to a file instead of a folder?
A: Yes. Mapping to a single file path is supported if that’s all you need.
123→ normalized toC123(changeset 123)C123→ changeset 123LMyLabel/label: "MyLabel"→ resolves to the changeset behind the labelT→ tip/latestD2025-09-01→ latest changeset on/after that date
- Install: Map & get per manifest; write/refresh lockfile.
- Restore: Get to exact
resolvedChangesets from lockfile. - Update: Re-resolve manifest versions/labels; sync; refresh lockfile.
- Remove: Unmap & delete local folders; CI also deletes workspaces.
<repo>/
tools/
TfvcPackageManager.ps1
config-specification.json
config-specification.lock.json
workspaces/ <-- defaultWorkspace (recommended outside repo if desired)
You’re set! Point your manifest at the TFVC content you need, then run install → update (to move forward) or restore (to reproduce exactly). remove keeps your machine and CI agents clean.