Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
name: Push Pages to Confluence
|
||||
|
||||
on:
|
||||
push:
|
||||
paths: ['pages/**']
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
push:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Write secrets file
|
||||
run: |
|
||||
"CONFLUENCE_TOKEN=${{ secrets.CONFLUENCE_TOKEN }}" | Out-File .secrets -Encoding UTF8
|
||||
|
||||
- name: Push pages to Confluence
|
||||
run: powershell -ExecutionPolicy Bypass -File scripts/confluence-push.ps1
|
||||
@@ -0,0 +1,3 @@
|
||||
.secrets
|
||||
data/export/
|
||||
*.log
|
||||
@@ -0,0 +1 @@
|
||||
CONFLUENCE_TOKEN=your_personal_access_token_here
|
||||
@@ -0,0 +1,74 @@
|
||||
# Agent Instructions — Confluence Bot
|
||||
|
||||
## Overview
|
||||
|
||||
This repo provides tools to analyze and maintain a Confluence space. As an AI agent, you can use these scripts to:
|
||||
|
||||
1. **Export** the page tree for analysis
|
||||
2. **Push** new or updated content as pages
|
||||
3. **Check** what pages exist
|
||||
4. **Read** specific pages for context
|
||||
5. **Reorganize** the page structure
|
||||
|
||||
## Setup (First Time)
|
||||
|
||||
1. Ensure `.secrets` exists with a valid `CONFLUENCE_TOKEN`
|
||||
2. Edit `config.ps1` with the correct Confluence URL, space key, and root page ID
|
||||
3. Test connection: `powershell -ExecutionPolicy Bypass -File scripts/confluence-check.ps1`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Analyzing a Space
|
||||
|
||||
```powershell
|
||||
# 1. Export everything
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-export.ps1
|
||||
|
||||
# 2. Read the exported files in data/export/pages/
|
||||
# 3. Analyze structure via data/export/page-index.json
|
||||
```
|
||||
|
||||
### Publishing Content
|
||||
|
||||
1. Create `.md` files in the `/pages/` directory
|
||||
2. Use standard Markdown (headers, lists, bold, code blocks)
|
||||
3. The filename or first `# Heading` becomes the page title
|
||||
4. Run: `powershell -ExecutionPolicy Bypass -File scripts/confluence-push.ps1`
|
||||
|
||||
### Reading Specific Pages
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-read.ps1 -PageIds "123456,789012"
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- All scripts use PowerShell (compatible with PS 5.1+)
|
||||
- Authentication is via Personal Access Token (PAT)
|
||||
- The token is loaded from `.secrets` file (never commit this!)
|
||||
- Pages are created/updated idempotently (safe to run multiple times)
|
||||
- Content comparison prevents unnecessary version bumps
|
||||
|
||||
## Error Handling
|
||||
|
||||
- `401/403` → Token expired or insufficient permissions
|
||||
- `Connection failed` → VPN required or wrong URL
|
||||
- `Page not found` → Wrong page ID in config
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
Confluence_Bot/
|
||||
├── config.ps1 # Your Confluence settings
|
||||
├── .secrets # Token (gitignored)
|
||||
├── pages/ # Markdown files to push
|
||||
├── data/ # Exported data (gitignored)
|
||||
│ ├── export/pages/ # Exported HTML + MD
|
||||
│ └── pages/ # Read pages
|
||||
└── scripts/
|
||||
├── lib/confluence-api.ps1 # Shared API functions
|
||||
├── confluence-export.ps1 # Full tree export
|
||||
├── confluence-push.ps1 # Push markdown as pages
|
||||
├── confluence-check.ps1 # List existing pages
|
||||
└── confluence-read.ps1 # Read specific pages
|
||||
```
|
||||
@@ -0,0 +1,78 @@
|
||||
# Confluence Bot
|
||||
|
||||
A standalone toolkit for analyzing and maintaining Confluence spaces. Clone this repo, configure your Confluence instance, and use the scripts to export, push, read, check, and reorganize pages.
|
||||
|
||||
## Features
|
||||
|
||||
- **Export** — Full page tree export (HTML + Markdown + JSON index)
|
||||
- **Push** — Create/update pages from local Markdown files with automatic Umlaut handling
|
||||
- **Read** — Read specific pages and their children
|
||||
- **Check** — Verify which pages exist under a parent
|
||||
- **Reorganize** — Move pages into a category structure
|
||||
- **Delete** — Remove test/draft pages
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Clone and configure
|
||||
|
||||
```bash
|
||||
git clone https://github.com/DoctoDre/Confluence_Bot.git my-confluence-project
|
||||
cd my-confluence-project
|
||||
cp .secrets.example .secrets
|
||||
# Edit .secrets with your Confluence PAT
|
||||
```
|
||||
|
||||
### 2. Set your Confluence details
|
||||
|
||||
Edit `config.ps1`:
|
||||
```powershell
|
||||
$ConfluenceUrl = "https://your-confluence.example.com"
|
||||
$SpaceKey = "YOUR_SPACE"
|
||||
$RootPageId = "123456789" # The root page ID to work under
|
||||
```
|
||||
|
||||
### 3. Run scripts
|
||||
|
||||
```powershell
|
||||
# Export entire page tree
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-export.ps1
|
||||
|
||||
# Push local markdown as pages
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-push.ps1
|
||||
|
||||
# Check what pages exist
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-check.ps1
|
||||
|
||||
# Read specific pages
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-read.ps1
|
||||
|
||||
# Reorganize pages into categories
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-reorganize.ps1
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### `.secrets` file
|
||||
```
|
||||
CONFLUENCE_TOKEN=your_personal_access_token_here
|
||||
```
|
||||
|
||||
### `config.ps1`
|
||||
```powershell
|
||||
$ConfluenceUrl = "https://your-confluence.example.com"
|
||||
$SpaceKey = "MYSPACE"
|
||||
$RootPageId = "123456789"
|
||||
$ApiBase = "$ConfluenceUrl/rest/api"
|
||||
```
|
||||
|
||||
## For AI Agents
|
||||
|
||||
See [AGENTS.md](AGENTS.md) for instructions on how to use this repo as an AI agent working on Confluence maintenance tasks.
|
||||
|
||||
## Setting Up a New Project
|
||||
|
||||
See [SETUP_NEW_PROJECT.md](SETUP_NEW_PROJECT.md) for step-by-step instructions on cloning this template for a new Confluence space.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,125 @@
|
||||
# Setting Up a New Confluence Project
|
||||
|
||||
Follow these steps to use this template for a new Confluence space.
|
||||
|
||||
## 1. Clone the Template
|
||||
|
||||
```bash
|
||||
git clone https://github.com/DoctoDre/Confluence_Bot.git my-new-confluence-project
|
||||
cd my-new-confluence-project
|
||||
|
||||
# Remove the original remote and set your own
|
||||
git remote remove origin
|
||||
git remote add origin https://github.com/YOUR_USER/my-new-confluence-project.git
|
||||
```
|
||||
|
||||
## 2. Configure
|
||||
|
||||
### Set your Confluence details
|
||||
|
||||
Edit `config.ps1`:
|
||||
```powershell
|
||||
$ConfluenceUrl = "https://your-confluence-instance.com"
|
||||
$SpaceKey = "YOUR_SPACE_KEY"
|
||||
$RootPageId = "123456789" # Find this in the page URL
|
||||
```
|
||||
|
||||
### Set your token
|
||||
|
||||
```bash
|
||||
cp .secrets.example .secrets
|
||||
# Edit .secrets and paste your Personal Access Token
|
||||
```
|
||||
|
||||
**How to get a PAT:**
|
||||
- Confluence Cloud: Profile → Settings → Personal Access Tokens → Create
|
||||
- Confluence Server: Profile → Personal Access Tokens → Create
|
||||
|
||||
## 3. Test Connection
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-check.ps1
|
||||
```
|
||||
|
||||
If successful, you'll see a list of child pages under your root page.
|
||||
|
||||
## 4. Initial Export (Optional)
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-export.ps1
|
||||
```
|
||||
|
||||
This downloads the entire page tree for local analysis.
|
||||
|
||||
## 5. Push Content
|
||||
|
||||
Create markdown files in `/pages/`:
|
||||
```
|
||||
pages/
|
||||
├── 01-overview.md
|
||||
├── 02-architecture.md
|
||||
└── 03-roadmap.md
|
||||
```
|
||||
|
||||
Then push:
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-push.ps1
|
||||
```
|
||||
|
||||
## 6. Set Up CI/CD (Optional)
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
Add these secrets to your repo:
|
||||
- `CONFLUENCE_TOKEN` — your PAT
|
||||
|
||||
Create `.github/workflows/confluence-push.yml`:
|
||||
```yaml
|
||||
name: Push to Confluence
|
||||
|
||||
on:
|
||||
push:
|
||||
paths: ['pages/**']
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
push:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Write secrets
|
||||
run: |
|
||||
"CONFLUENCE_TOKEN=${{ secrets.CONFLUENCE_TOKEN }}" | Out-File .secrets -Encoding UTF8
|
||||
- name: Push pages
|
||||
run: powershell -ExecutionPolicy Bypass -File scripts/confluence-push.ps1
|
||||
```
|
||||
|
||||
### GitLab CI
|
||||
|
||||
Add `CONFLUENCE_TOKEN` as a CI/CD variable, then create `.gitlab-ci.yml`:
|
||||
```yaml
|
||||
push-confluence:
|
||||
stage: deploy
|
||||
tags: [windows]
|
||||
only:
|
||||
changes:
|
||||
- pages/**
|
||||
script:
|
||||
- echo "CONFLUENCE_TOKEN=$CONFLUENCE_TOKEN" > .secrets
|
||||
- powershell -ExecutionPolicy Bypass -File scripts/confluence-push.ps1
|
||||
```
|
||||
|
||||
## 7. Push to Your Repo
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Initial setup for my Confluence space"
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- **Page titles** come from the first `# Heading` in each markdown file
|
||||
- **Idempotent** — running push multiple times won't create duplicates
|
||||
- **Version tracking** — only updates pages when content actually changed
|
||||
- **Safe** — never deletes pages unless you explicitly use the delete function
|
||||
@@ -0,0 +1,31 @@
|
||||
# Confluence Bot Configuration
|
||||
# Edit these values for your Confluence instance
|
||||
|
||||
$ConfluenceUrl = "https://your-confluence.example.com"
|
||||
$SpaceKey = "YOUR_SPACE"
|
||||
$RootPageId = "123456789" # Root page ID to work under
|
||||
$ApiBase = "$ConfluenceUrl/rest/api"
|
||||
$PerPage = 50
|
||||
|
||||
# Output directories
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ProjectDir = Split-Path -Parent $ScriptDir
|
||||
$OutputDir = Join-Path $ProjectDir "data/export"
|
||||
$PagesDir = Join-Path $ProjectDir "pages"
|
||||
|
||||
# TLS
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# Load token from .secrets
|
||||
$SecretsPath = Join-Path $ProjectDir ".secrets"
|
||||
$PAT = $null
|
||||
if (Test-Path $SecretsPath) {
|
||||
Get-Content $SecretsPath | ForEach-Object {
|
||||
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
|
||||
$val = $Matches[1].Trim()
|
||||
if ($val -ne "your_personal_access_token_here") { $PAT = $val }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $PAT) { $PAT = Read-Host "Confluence Personal Access Token" }
|
||||
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
|
||||
@@ -0,0 +1 @@
|
||||
# Place your .md files here to push to Confluence
|
||||
@@ -0,0 +1,24 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check which pages exist under the configured root page
|
||||
.USAGE
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-check.ps1
|
||||
#>
|
||||
|
||||
. "$PSScriptRoot/../config.ps1"
|
||||
. "$PSScriptRoot/lib/confluence-api.ps1"
|
||||
|
||||
Write-Host "=== Confluence Page Check ===" -ForegroundColor Cyan
|
||||
Write-Host "Root: $RootPageId | Space: $SpaceKey"
|
||||
Write-Host ""
|
||||
|
||||
$Children = Get-ChildPages -PageId $RootPageId
|
||||
Write-Host "Found $($Children.Count) child pages:" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host ("{0,-10} {1,-60} {2}" -f "ID", "Title", "Version")
|
||||
Write-Host ("-" * 80)
|
||||
|
||||
$Children | Sort-Object { $_.title } | ForEach-Object {
|
||||
$ver = if ($_.version) { $_.version.number } else { "?" }
|
||||
Write-Host ("{0,-10} {1,-60} v{2}" -f $_.id, $_.title, $ver)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Export a Confluence space page tree to local files (HTML + Markdown + JSON index)
|
||||
.USAGE
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-export.ps1
|
||||
#>
|
||||
|
||||
# Load config and shared functions
|
||||
. "$PSScriptRoot/../config.ps1"
|
||||
. "$PSScriptRoot/lib/confluence-api.ps1"
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " Confluence Export - $SpaceKey"
|
||||
Write-Host "============================================================"
|
||||
Write-Host " URL: $ConfluenceUrl"
|
||||
Write-Host " Root Page: $RootPageId"
|
||||
Write-Host ""
|
||||
|
||||
function Export-PageTree {
|
||||
param([string]$PageId, [string]$ParentPath, [int]$Depth = 0, [ref]$Counter)
|
||||
$Indent = " " * $Depth
|
||||
$Counter.Value++
|
||||
|
||||
$Page = Get-PageContent -PageId $PageId
|
||||
if ($null -eq $Page) { return $null }
|
||||
|
||||
$Title = [string]$Page.title
|
||||
Write-Host "$Indent[$($Counter.Value)] $Title"
|
||||
|
||||
$Labels = @()
|
||||
if ($Page.metadata -and $Page.metadata.labels -and $Page.metadata.labels.results) {
|
||||
$Labels = @($Page.metadata.labels.results | ForEach-Object { $_.name })
|
||||
}
|
||||
$VersionNum = if ($Page.version) { $Page.version.number } else { 0 }
|
||||
|
||||
$PageInfo = [PSCustomObject]@{
|
||||
id = $Page.id; title = $Title; version = $VersionNum
|
||||
labels = $Labels; depth = $Depth; path = "$ParentPath/$Title"; children = @()
|
||||
}
|
||||
|
||||
# Save content
|
||||
$HtmlContent = ""
|
||||
$TextContent = ""
|
||||
if ($Page.body -and $Page.body.storage) {
|
||||
$HtmlContent = [string]$Page.body.storage.value
|
||||
$TextContent = Convert-HtmlToText $HtmlContent
|
||||
}
|
||||
|
||||
$SafeTitle = $Title -replace '[\\/:*?"<>|]', '_'
|
||||
if ($SafeTitle.Length -gt 80) { $SafeTitle = $SafeTitle.Substring(0, 80) }
|
||||
$PageDir = Join-Path $OutputDir "pages"
|
||||
New-Item -ItemType Directory -Path $PageDir -Force | Out-Null
|
||||
|
||||
# Markdown
|
||||
$MdLines = @("# $Title", "", "> Page ID: $($Page.id)", "> Version: $VersionNum", "> Path: $($PageInfo.path)", "> Labels: $(($Labels) -join ', ')", "", "---", "", $TextContent)
|
||||
$MdPath = Join-Path $PageDir "$($Page.id)_$SafeTitle.md"
|
||||
$MdLines -join "`n" | Out-File -FilePath $MdPath -Encoding UTF8
|
||||
|
||||
# HTML
|
||||
$HtmlPath = Join-Path $PageDir "$($Page.id)_$SafeTitle.html"
|
||||
$HtmlContent | Out-File -FilePath $HtmlPath -Encoding UTF8
|
||||
|
||||
# Children
|
||||
$Children = @(Get-ChildPages -PageId $PageId)
|
||||
$ChildInfos = @()
|
||||
foreach ($Child in $Children) {
|
||||
$ChildInfo = Export-PageTree -PageId $Child.id -ParentPath $PageInfo.path -Depth ($Depth + 1) -Counter $Counter
|
||||
if ($null -ne $ChildInfo) { $ChildInfos += $ChildInfo }
|
||||
}
|
||||
$PageInfo.children = $ChildInfos
|
||||
return $PageInfo
|
||||
}
|
||||
|
||||
# Main
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
Write-Host ">> Testing connection..." -ForegroundColor Yellow
|
||||
$TestResult = Invoke-ConfluenceApi -Endpoint "/space/$SpaceKey"
|
||||
if ($null -eq $TestResult) { Write-Host " Connection failed." -ForegroundColor Red; exit 1 }
|
||||
Write-Host " Space: $($TestResult.name) ($SpaceKey)" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ">> Exporting page tree from $RootPageId..." -ForegroundColor Yellow
|
||||
$Counter = [ref]0
|
||||
$Tree = Export-PageTree -PageId $RootPageId -ParentPath "" -Depth 0 -Counter $Counter
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ">> $($Counter.Value) pages exported" -ForegroundColor Green
|
||||
|
||||
# JSON index
|
||||
$JsonPath = Join-Path $OutputDir "page-index.json"
|
||||
$Tree | ConvertTo-Json -Depth 20 | Out-File -FilePath $JsonPath -Encoding UTF8
|
||||
Write-Host " Index: $JsonPath"
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,88 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Push local markdown files from /pages/ folder as Confluence pages
|
||||
.DESCRIPTION
|
||||
Reads all .md files from the /pages/ directory, converts them to HTML,
|
||||
and creates/updates them as child pages under the configured root page.
|
||||
.USAGE
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-push.ps1
|
||||
#>
|
||||
|
||||
. "$PSScriptRoot/../config.ps1"
|
||||
. "$PSScriptRoot/lib/confluence-api.ps1"
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " Confluence Push - $SpaceKey"
|
||||
Write-Host "============================================================"
|
||||
Write-Host " Target: $ConfluenceUrl/spaces/$SpaceKey"
|
||||
Write-Host " Parent: $RootPageId"
|
||||
Write-Host ""
|
||||
|
||||
# Test connection
|
||||
Write-Host ">> Testing connection..." -ForegroundColor Yellow
|
||||
$Test = Invoke-ConfluenceApi -Endpoint "/content/$RootPageId"
|
||||
if (-not $Test) { Write-Host " Connection failed!" -ForegroundColor Red; exit 1 }
|
||||
Write-Host " Target page: $($Test.title)" -ForegroundColor Green
|
||||
|
||||
# Find markdown files to push
|
||||
$PagesPath = Join-Path $ProjectDir "pages"
|
||||
if (-not (Test-Path $PagesPath)) {
|
||||
Write-Host " No /pages/ directory found. Create it and add .md files." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
$MdFiles = Get-ChildItem -Path $PagesPath -Filter "*.md" | Sort-Object Name
|
||||
if ($MdFiles.Count -eq 0) {
|
||||
Write-Host " No .md files found in /pages/" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ">> Pushing $($MdFiles.Count) pages..." -ForegroundColor Yellow
|
||||
|
||||
function Convert-MdToHtml($mdContent) {
|
||||
# Simple markdown to Confluence storage format conversion
|
||||
$html = $mdContent
|
||||
# Headers
|
||||
$html = $html -replace '^#### (.+)$', '<h4>$1</h4>'
|
||||
$html = $html -replace '^### (.+)$', '<h3>$1</h3>'
|
||||
$html = $html -replace '^## (.+)$', '<h2>$1</h2>'
|
||||
$html = $html -replace '^# (.+)$', '<h1>$1</h1>'
|
||||
# Bold/italic
|
||||
$html = $html -replace '\*\*(.+?)\*\*', '<strong>$1</strong>'
|
||||
$html = $html -replace '\*(.+?)\*', '<em>$1</em>'
|
||||
# Lists
|
||||
$html = $html -replace '^- (.+)$', '<li>$1</li>'
|
||||
# Code blocks
|
||||
$html = $html -replace '```(\w*)\n([\s\S]*?)```', '<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">$1</ac:parameter><ac:plain-text-body><![CDATA[$2]]></ac:plain-text-body></ac:structured-macro>'
|
||||
# Inline code
|
||||
$html = $html -replace '`(.+?)`', '<code>$1</code>'
|
||||
# Paragraphs (lines not already tagged)
|
||||
$lines = $html -split "`n"
|
||||
$result = @()
|
||||
foreach ($line in $lines) {
|
||||
if ($line -match '^<' -or $line.Trim() -eq '') {
|
||||
$result += $line
|
||||
} else {
|
||||
$result += "<p>$line</p>"
|
||||
}
|
||||
}
|
||||
return $result -join "`n"
|
||||
}
|
||||
|
||||
foreach ($File in $MdFiles) {
|
||||
$Content = Get-Content $File.FullName -Raw -Encoding UTF8
|
||||
# Use filename (without .md) as page title, or first # heading
|
||||
$Title = $File.BaseName
|
||||
if ($Content -match '^# (.+)$') { $Title = $Matches[1] }
|
||||
|
||||
Write-Host " $Title..." -NoNewline
|
||||
$HtmlBody = Convert-MdToHtml $Content
|
||||
$PageId = Create-Or-Update-Page -Title $Title -HtmlBody $HtmlBody -ParentId $RootPageId
|
||||
if ($PageId) { Write-Host "" }
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " Done! $($MdFiles.Count) pages processed." -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,41 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Read specific Confluence pages by ID and save locally
|
||||
.USAGE
|
||||
powershell -ExecutionPolicy Bypass -File scripts/confluence-read.ps1 -PageIds "123,456,789"
|
||||
#>
|
||||
param([string]$PageIds = "")
|
||||
|
||||
. "$PSScriptRoot/../config.ps1"
|
||||
. "$PSScriptRoot/lib/confluence-api.ps1"
|
||||
|
||||
Write-Host "=== Confluence Page Reader ===" -ForegroundColor Cyan
|
||||
|
||||
if (-not $PageIds) {
|
||||
$PageIds = Read-Host "Enter page IDs (comma-separated)"
|
||||
}
|
||||
|
||||
$Ids = $PageIds -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
$OutPath = Join-Path $ProjectDir "data/pages"
|
||||
New-Item -ItemType Directory -Path $OutPath -Force | Out-Null
|
||||
|
||||
foreach ($Id in $Ids) {
|
||||
Write-Host ""
|
||||
Write-Host ">> Page $Id" -ForegroundColor Yellow
|
||||
$Page = Get-PageContent -PageId $Id
|
||||
if (-not $Page) { continue }
|
||||
|
||||
Write-Host " Title: $($Page.title) (v$($Page.version.number))" -ForegroundColor Green
|
||||
|
||||
$HtmlContent = ""
|
||||
if ($Page.body -and $Page.body.storage) { $HtmlContent = $Page.body.storage.value }
|
||||
$TextContent = Convert-HtmlToText $HtmlContent
|
||||
|
||||
$SafeTitle = $Page.title -replace '[\\/:*?"<>|]', '_'
|
||||
$HtmlContent | Out-File "$OutPath/$($Page.id)_$SafeTitle.html" -Encoding UTF8
|
||||
$TextContent | Out-File "$OutPath/$($Page.id)_$SafeTitle.md" -Encoding UTF8
|
||||
Write-Host " Saved ($($HtmlContent.Length) chars)"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Done! Files in $OutPath" -ForegroundColor Green
|
||||
@@ -0,0 +1,160 @@
|
||||
# Shared Confluence API functions
|
||||
# Source this file in other scripts: . "$PSScriptRoot/lib/confluence-api.ps1"
|
||||
|
||||
function Invoke-ConfluenceApi {
|
||||
param([string]$Endpoint, [hashtable]$Params = @{}, [string]$Method = "Get", [string]$Body = $null)
|
||||
$QS = ($Params.GetEnumerator() | ForEach-Object {
|
||||
"$($_.Key)=$([Uri]::EscapeDataString($_.Value))"
|
||||
}) -join "&"
|
||||
$Uri = "$ApiBase$Endpoint"
|
||||
if ($QS) { $Uri = "$Uri`?$QS" }
|
||||
try {
|
||||
if ($Body) {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing -ErrorAction Stop
|
||||
} else {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
}
|
||||
return ($Resp.Content | ConvertFrom-Json)
|
||||
} catch {
|
||||
Write-Host " ERROR at $Uri : $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ChildPages {
|
||||
param([string]$PageId)
|
||||
$AllChildren = @()
|
||||
$Start = 0
|
||||
do {
|
||||
$Result = Invoke-ConfluenceApi -Endpoint "/content/$PageId/child/page" -Params @{
|
||||
start = "$Start"
|
||||
limit = "$PerPage"
|
||||
expand = "version"
|
||||
}
|
||||
if ($null -eq $Result) { break }
|
||||
$Pages = $Result.results
|
||||
if ($null -eq $Pages -or $Pages.Count -eq 0) { break }
|
||||
$AllChildren += $Pages
|
||||
$Start += $PerPage
|
||||
$Size = 0
|
||||
if ($Result.size) { $Size = $Result.size }
|
||||
} while ($Size -ge $PerPage)
|
||||
return $AllChildren
|
||||
}
|
||||
|
||||
function Get-PageContent {
|
||||
param([string]$PageId)
|
||||
return Invoke-ConfluenceApi -Endpoint "/content/$PageId" -Params @{
|
||||
expand = "body.storage,version,ancestors,metadata.labels,space"
|
||||
}
|
||||
}
|
||||
|
||||
function Convert-HtmlToText($html) {
|
||||
if ($null -eq $html) { return "" }
|
||||
$text = $html -replace '<br\s*/?>', "`n"
|
||||
$text = $text -replace '<p[^>]*>', "`n"
|
||||
$text = $text -replace '</p>', ""
|
||||
$text = $text -replace '<li[^>]*>', "`n- "
|
||||
$text = $text -replace '<h[1-6][^>]*>', "`n## "
|
||||
$text = $text -replace '</h[1-6]>', "`n"
|
||||
$text = $text -replace '<tr[^>]*>', "`n| "
|
||||
$text = $text -replace '<td[^>]*>', " | "
|
||||
$text = $text -replace '<th[^>]*>', " | "
|
||||
$text = $text -replace '<[^>]+>', ''
|
||||
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
|
||||
try { $text = [System.Web.HttpUtility]::HtmlDecode($text) } catch {}
|
||||
return $text.Trim()
|
||||
}
|
||||
|
||||
function Create-Or-Update-Page {
|
||||
param([string]$Title, [string]$HtmlBody, [string]$ParentId)
|
||||
|
||||
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($Title))&type=page"
|
||||
try {
|
||||
$SearchResp = Invoke-WebRequest -Uri $SearchUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$SearchData = $SearchResp.Content | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-Host " ERROR searching: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($SearchData.results -and $SearchData.results.Count -gt 0) {
|
||||
$PageId = $SearchData.results[0].id
|
||||
try {
|
||||
$VerResp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId`?expand=version,body.storage" -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$VerData = $VerResp.Content | ConvertFrom-Json
|
||||
$Version = $VerData.version.number + 1
|
||||
$CurrentBody = ""
|
||||
if ($VerData.body -and $VerData.body.storage) { $CurrentBody = $VerData.body.storage.value }
|
||||
$NormCurrent = ($CurrentBody -replace '<[^>]+>' -replace '&\w+;' -replace '\s+', ' ').Trim()
|
||||
$NormNew = ($HtmlBody -replace '<[^>]+>' -replace '&\w+;' -replace '\s+', ' ').Trim()
|
||||
if ($NormCurrent -eq $NormNew) {
|
||||
Write-Host " No changes (ID: $PageId)" -ForegroundColor DarkGray
|
||||
return $PageId
|
||||
}
|
||||
} catch {
|
||||
$Version = $SearchData.results[0].version.number + 1
|
||||
}
|
||||
|
||||
Write-Host " Updating (ID: $PageId, v$Version)..." -NoNewline
|
||||
$Body = @{
|
||||
version = @{ number = $Version }
|
||||
title = $Title
|
||||
type = "page"
|
||||
body = @{ storage = @{ value = $HtmlBody; representation = "storage" } }
|
||||
} | ConvertTo-Json -Depth 10
|
||||
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Method Put -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing -ErrorAction Stop
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
return ($Resp.Content | ConvertFrom-Json).id
|
||||
} catch {
|
||||
Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
} else {
|
||||
Write-Host " Creating new page..." -NoNewline
|
||||
$Body = @{
|
||||
type = "page"
|
||||
title = $Title
|
||||
space = @{ key = $SpaceKey }
|
||||
ancestors = @(@{ id = $ParentId })
|
||||
body = @{ storage = @{ value = $HtmlBody; representation = "storage" } }
|
||||
} | ConvertTo-Json -Depth 10
|
||||
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri "$ApiBase/content" -Method Post -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing -ErrorAction Stop
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
return ($Resp.Content | ConvertFrom-Json).id
|
||||
} catch {
|
||||
Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Move-Page($PageId, $NewParentId) {
|
||||
$Page = Invoke-ConfluenceApi -Endpoint "/content/$PageId" -Params @{ expand = "version,ancestors" }
|
||||
if (-not $Page) { return $false }
|
||||
if ($Page.ancestors -and $Page.ancestors.Count -gt 0) {
|
||||
if ($Page.ancestors[-1].id -eq $NewParentId) { return $true }
|
||||
}
|
||||
$Version = $Page.version.number + 1
|
||||
$Json = @{
|
||||
type = "page"; title = $Page.title
|
||||
version = @{ number = $Version }
|
||||
ancestors = @(@{ id = $NewParentId })
|
||||
} | ConvertTo-Json -Depth 10
|
||||
$Result = Invoke-ConfluenceApi -Endpoint "/content/$PageId" -Method "Put" -Body $Json
|
||||
return ($null -ne $Result)
|
||||
}
|
||||
|
||||
function Delete-Page($PageId) {
|
||||
try {
|
||||
Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Method Delete -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
return $true
|
||||
} catch {
|
||||
Write-Host " ERROR deleting $PageId : $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user