Last Updated: 2025-11-11 Issue Tracking: Critical discovery during PDR documentation migration
This guide documents a critical behavior of the upload_confluence.py script related to parent-child page relationships, the root cause of inadvertent page moves, and the prevention strategies implemented.
- The Parent Relationship Issue
- Root Cause Analysis
- Prevention: New CLI Options
- Usage Examples
- Migration Workflow Patterns
- Troubleshooting
During a large-scale Confluence documentation restructure (PDR migration), we discovered that pages moved to new parent locations were inadvertently moved back to their original parents during a content restoration operation.
Timeline:
- Phase 2-3: Moved 10 pages to new parent locations
- Content Restoration: Restored original content from backup files using
upload_confluence.py - Discovery: Pages were back in original parent locations
Impact:
- 8 of 10 pages moved back to original parents
- Required re-moving all affected pages
- ~30 minutes of work to recover
The upload_confluence.py script reads YAML frontmatter from markdown backup files. This frontmatter includes a parent.id field:
---
title: "PDR Data Flows"
confluence:
id: 218695766
version: 4
parent:
id: 205131485 # ← Original parent before migration
title: "Perimeter Data Router (PDR)"
---Default Script Behavior (before fix):
- Parse YAML frontmatter from backup file
- Extract
parent.idfield (original parent location) - Call
confluence.update_page()with this parent ID - Result: Page moved back to original parent
python3 upload_confluence.py \
--id 218695766 \
--env-file /path/to/.env.jira \
PDR_Data_Flows.mdIntention: Update page content only Actual Result: Updated content AND moved page to parent specified in frontmatter
The frontmatter-based parent handling was designed for the download → edit → upload workflow:
# 1. Download page (includes current parent in frontmatter)
python3 download_confluence.py 218695766
# 2. Edit locally
vim PDR_Data_Flows.md
# 3. Upload (preserves parent relationship)
python3 upload_confluence.py PDR_Data_Flows.mdThis workflow is correct when the page hasn't been moved in between.
The issue occurs when:
- Page is moved to new parent via API
- Backup file still has old parent ID in frontmatter
- Upload script restores content using backup file
- Page is inadvertently moved back to old parent
Added two new CLI options to upload_confluence.py to give explicit control over parent relationship handling:
--parent-id PARENT_ID # Explicitly specify parent (overrides frontmatter)
--ignore-frontmatter # Ignore parent.id from frontmatter entirelyPurpose: Update page content in place without changing parent relationship.
Behavior:
- Ignores
parent.idfrom YAML frontmatter - Only uses
--parent-idif explicitly provided on command line - If no
--parent-idprovided, parent remains unchanged
When to Use:
- Content-only updates
- Restoring content after moves
- Updating pages without changing structure
- Avoiding inadvertent moves
Example:
# Update content only, preserve current parent
python3 upload_confluence.py --id 218695766 --ignore-frontmatter page.mdPurpose: Explicitly specify the parent page, overriding frontmatter.
Behavior:
- Sets parent to specified ID
- Overrides frontmatter if present
- Combined with
--ignore-frontmatter: only uses this explicit parent
When to Use:
- Moving pages to new parent locations
- Correcting parent relationships
- Migration operations
- Explicit restructuring
Examples:
# Move page to new parent, update content
python3 upload_confluence.py --id 218695766 --parent-id 763331326 page.md
# Move to new parent, ignore frontmatter parent
python3 upload_confluence.py --id 218695766 --parent-id 763331326 --ignore-frontmatter page.mdGoal: Update page content without changing parent.
Command:
python3 upload_confluence.py \
--id 450855912 \
--ignore-frontmatter \
--env-file ~/.env.confluence \
my_page.mdWhat Happens:
- ✅ Content updated
- ✅ Version incremented
- ❌ Parent NOT changed (remains as-is)
- ❌ Frontmatter parent.id ignored
Use Case: Content restoration after moves, bug fixes, content updates
Goal: Update content AND move to new parent.
Command:
python3 upload_confluence.py \
--id 450855912 \
--parent-id 763331326 \
--ignore-frontmatter \
--env-file ~/.env.confluence \
my_page.mdWhat Happens:
- ✅ Content updated
- ✅ Version incremented
- ✅ Parent changed to 763331326
- ❌ Frontmatter parent.id ignored
Use Case: Migration operations, restructuring, controlled moves
Goal: Use frontmatter parent (original behavior).
Command:
python3 upload_confluence.py \
--id 450855912 \
--env-file ~/.env.confluence \
my_page.mdWhat Happens:
- ✅ Content updated
- ✅ Version incremented
⚠️ Parent set from frontmatter (if present)
Use Case: Download → edit → upload workflow (when page hasn't been moved)
Goal: Create new page under specific parent.
Command:
python3 upload_confluence.py \
--space ARCP \
--parent-id 205131485 \
--env-file ~/.env.confluence \
new_page.mdWhat Happens:
- ✅ New page created
- ✅ Parent set to 205131485
- ✅ Space ARCP
Use Case: Creating new documentation pages
Goal: Let frontmatter specify everything (when frontmatter is correct).
Command:
python3 upload_confluence.py my_page.mdFrontmatter:
---
title: My Page
confluence:
id: 450855912
space: ARCP
version: 5
parent:
id: 763331326
---What Happens:
- ✅ Page 450855912 updated
- ✅ Version 5 → 6
⚠️ Parent set to 763331326 from frontmatter
Use Case: When frontmatter is accurate and up-to-date
Scenario: You've moved pages to new parents, then need to restore original content.
Steps:
# 1. Move pages to new parents (already done via API)
# Pages now in correct parent locations
# 2. Restore content from backups (DO NOT move back!)
python3 upload_confluence.py \
--id 218695766 \
--ignore-frontmatter \
backup/PDR_Data_Flows.mdKey: --ignore-frontmatter prevents moving back to old parent in backup file.
Scenario: Moving and updating multiple pages.
Steps:
# Move Architecture pages to Architecture & Design section
for page_id in 218695766 479822004 205157992; do
python3 upload_confluence.py \
--id "$page_id" \
--parent-id 763331326 \
--ignore-frontmatter \
"backups/page_${page_id}.md"
doneKey: Explicit --parent-id + --ignore-frontmatter = full control.
Scenario: Normal workflow when page hasn't been moved.
Steps:
# 1. Download page (gets current parent in frontmatter)
python3 download_confluence.py 218695766
# Creates PDR_Data_Flows.md with frontmatter
# 2. Edit locally
vim PDR_Data_Flows.md
# 3. Upload (frontmatter parent is current/correct)
python3 upload_confluence.py PDR_Data_Flows.mdKey: Frontmatter parent matches current location = safe.
Scenario: Large-scale restructure with validation.
Steps:
# 1. Dry-run to preview
python3 upload_confluence.py \
--id 218695766 \
--parent-id 763331326 \
--ignore-frontmatter \
--dry-run \
my_page.md
# 2. Execute move + content update
python3 upload_confluence.py \
--id 218695766 \
--parent-id 763331326 \
--ignore-frontmatter \
my_page.md
# 3. Validate parent relationship via MCP
# (Use mcp__atlassian-evinova__confluence_get_page)Key: Always validate after moves in critical migrations.
Symptoms:
- Page moved to new parent
- After content update, page is back in old parent
Root Cause: Frontmatter contains old parent ID
Solution:
# Use --ignore-frontmatter for content-only updates
python3 upload_confluence.py --id PAGE_ID --ignore-frontmatter page.mdSymptoms:
--parent-idspecified but page doesn't move- Page remains in current location
Possible Causes:
- Page already in specified parent
- Permission issues
- Parent page doesn't exist
Debugging:
# 1. Verify current parent
python3 -c "
from confluence_auth import get_confluence_client
conf = get_confluence_client()
page = conf.get_page_by_id('PAGE_ID', expand='ancestors')
print(page.get('ancestors', []))
"
# 2. Verify target parent exists
python3 -c "
from confluence_auth import get_confluence_client
conf = get_confluence_client()
parent = conf.get_page_by_id('PARENT_ID')
print(parent['title'])
"
# 3. Try with verbose output
python3 upload_confluence.py --id PAGE_ID --parent-id PARENT_ID --dry-run page.mdSymptoms:
- Frontmatter says one parent
- Want to move to different parent
- Confused which will be used
Solution:
# Explicit parent always wins when combined with --ignore-frontmatter
python3 upload_confluence.py \
--id PAGE_ID \
--parent-id NEW_PARENT_ID \
--ignore-frontmatter \
page.md| Goal | Command Pattern | Frontmatter Used? | Parent Changes? |
|---|---|---|---|
| Update content only | --id X --ignore-frontmatter |
❌ No | ❌ No |
| Update content + move | --id X --parent-id Y --ignore-frontmatter |
❌ No | ✅ Yes (to Y) |
| Update using frontmatter | --id X |
✅ Yes | |
| Create with parent | --space S --parent-id Y |
✅ Yes (to Y) | |
| Create from frontmatter | (no args) | ✅ Yes |
Legend:
- ✅ = Always
- ❌ = Never
⚠️ = Conditional
✅ DO: Always use --ignore-frontmatter when restoring content after moves
python3 upload_confluence.py --id X --ignore-frontmatter backup.md❌ DON'T: Rely on frontmatter parent after manual moves
# RISKY: May move page back to old parent
python3 upload_confluence.py --id X backup.md✅ DO: Use explicit --parent-id + --ignore-frontmatter for full control
python3 upload_confluence.py --id X --parent-id Y --ignore-frontmatter page.md✅ DO: Use --dry-run to preview before executing
python3 upload_confluence.py --id X --parent-id Y --dry-run page.md✅ DO: Validate parent-child relationships after moves
✅ DO: Use download → edit → upload when page hasn't been moved
python3 download_confluence.py X
vim page.md
python3 upload_confluence.py page.md # Safe: frontmatter is currentFile: /Users/richardhightower/.claude/skills/confluence/scripts/upload_confluence.py
Added CLI Arguments:
parser.add_argument('--parent-id', type=str,
help='Parent page ID (specify parent to move page)')
parser.add_argument('--ignore-frontmatter', action='store_true',
help='Ignore parent_id in frontmatter (update page in place without moving)')Updated Parent Logic:
# Handle parent_id with --ignore-frontmatter option
if args.ignore_frontmatter:
# Only use --parent-id if explicitly provided, don't read from frontmatter
parent_id = args.parent_id
else:
# Default behavior: CLI --parent-id overrides frontmatter
parent_id = args.parent_id or frontmatter.get('parent', {}).get('id')Behavior Matrix:
--ignore-frontmatter |
--parent-id |
Frontmatter parent.id |
Result |
|---|---|---|---|
| ❌ No | ❌ None | ❌ None | No parent set |
| ❌ No | ❌ None | ✅ 123 | Parent = 123 (from frontmatter) |
| ❌ No | ✅ 456 | ❌ None | Parent = 456 (explicit) |
| ❌ No | ✅ 456 | ✅ 123 | Parent = 456 (explicit overrides) |
| ✅ Yes | ❌ None | ✅ 123 | No parent change (ignored) |
| ✅ Yes | ✅ 456 | ✅ 123 | Parent = 456 (explicit only) |
Project: Perimeter Data Router (PDR) documentation restructure
Scope: 72 pages reorganized into new 7-section hierarchy
Tool Used: upload_confluence.py for content restoration
Phase 2-3: Moved 10 pages to new parents using MCP API
Content Restoration: Used upload_confluence.py with backup files
Result: 8 pages moved back to original parents
| Page | Original Parent | Moved To (Phase 2) | Restored To (Issue) | Re-Moved To |
|---|---|---|---|---|
| PDR Data Flows | PDR Main | Architecture & Design | PDR Main ❌ | Architecture & Design ✅ |
| PDR Events | PDR Main | Architecture & Design | PDR Main ❌ | Architecture & Design ✅ |
| PDR Requirements | PDR Main | Architecture & Design | PDR Main ❌ | Architecture & Design ✅ |
| Deployment Guides | PDR Main | Operations Guides | PDR Main ❌ | Operations Guides ✅ |
| Archived Pages | PDR Main | Reference | PDR Main ❌ | Reference ✅ |
| Future Requirements | PDR Main | Reference | PDR Main ❌ | Reference ✅ |
| Testing PDR | PDR User Guides | Developer Guides | PDR User Guides ❌ | Developer Guides ✅ |
| PDR Components | PDR Main | Architecture & Design | (No move - different method) | Architecture & Design ✅ |
Commands Used:
# Re-move to Architecture & Design (763331326)
for id in 218695766 479822004 205157992; do
python3 upload_confluence.py --id "$id" --parent-id 763331326 --ignore-frontmatter "backup/page_${id}.md"
done
# Re-move to Reference (763461928)
for id in 479821832 544112775; do
python3 upload_confluence.py --id "$id" --parent-id 763461928 --ignore-frontmatter "backup/page_${id}.md"
done
# Re-move to Developer Guides (766836740)
python3 upload_confluence.py --id 281904231 --parent-id 766836740 --ignore-frontmatter backup/page_281904231.md
# Re-move to Operations Guides (763331351)
python3 upload_confluence.py --id 646053890 --parent-id 763331351 --ignore-frontmatter backup/page_646053890.mdTime to Recover: ~15 minutes
Content Loss: Zero (all content preserved)
Lessons Learned: Always use --ignore-frontmatter for content restoration
-
Frontmatter Update Option (planned):
--update-frontmatter # Update frontmatter with current parent after move -
Parent Validation:
- Verify parent exists before move
- Warn if parent in frontmatter differs from current parent
-
Backup Safety:
- Compare frontmatter parent with current parent
- Warn if they differ (potential inadvertent move)
-
Move Detection:
- Detect when page has been moved since backup
- Prompt for confirmation before using frontmatter parent
Key Takeaways:
- ✅ Use
--ignore-frontmatterfor content-only updates after moves - ✅ Use
--parent-id+--ignore-frontmatterfor controlled moves - ✅ Use
--dry-runto preview operations - ✅ Validate parent relationships after migrations
⚠️ Be cautious with frontmatter parent when page has been moved
Quick Reference:
# Content only (safe)
--id X --ignore-frontmatter
# Content + move (controlled)
--id X --parent-id Y --ignore-frontmatter
# Preview first (recommended)
--dry-runDocument Created: 2025-11-11 Author: Evinova Agent Documentation Related: PDR Migration Phase 2-3 Content Restoration Issue