Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions .github/workflows/blog-to-basehub.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# Blog to Basehub CMS Automation
#
# This workflow automatically creates draft blog posts in Basehub CMS
# when new blog posts are added to the repository.
#
# Triggers:
# - Push to main with changes in blog/ directory
# - Manual dispatch with optional blog file path
#
# Required secrets:
# - BASEHUB_TOKEN: Admin API token from Basehub (Settings → API Tokens)
# - BASEHUB_REPO: Your Basehub repository ID (e.g., "your-org/your-repo")
#
# Setup:
# 1. Get your Basehub admin token from basehub.com → Your Repo → Settings → API Tokens
# 2. Add BASEHUB_TOKEN and BASEHUB_REPO as repository secrets
# 3. Ensure your Basehub repo has a "posts" or "blog" collection with compatible schema

name: Blog to Basehub CMS

on:
push:
branches: [main]
paths:
- 'blog/**/*.mdx'
- 'blog/**/*.md'
workflow_dispatch:
inputs:
blog_file:
description: 'Specific blog file to sync (e.g., blog/my-post.mdx)'
required: false
type: string
force_update:
description: 'Force update even if post exists'
required: false
type: boolean
default: false

jobs:
sync-to-basehub:
runs-on: ubuntu-24.04
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2 # Need previous commit to detect changes

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Install dependencies
run: npm install gray-matter slugify

- name: Detect changed blog files
id: detect-files
run: |
if [ -n "${{ github.event.inputs.blog_file }}" ]; then
# Manual dispatch with specific file
echo "files=${{ github.event.inputs.blog_file }}" >> $GITHUB_OUTPUT
else
# Auto-detect changed files
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD -- 'blog/*.mdx' 'blog/*.md' 2>/dev/null | tr '\n' ' ' || echo "")

# If no previous commit (first push), get all blog files
if [ -z "$CHANGED_FILES" ]; then
CHANGED_FILES=$(find blog -name "*.mdx" -o -name "*.md" 2>/dev/null | tr '\n' ' ' || echo "")
fi

echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT
fi

- name: Sync blog posts to Basehub
if: steps.detect-files.outputs.files != ''
env:
BASEHUB_TOKEN: ${{ secrets.BASEHUB_TOKEN }}
BASEHUB_REPO: ${{ secrets.BASEHUB_REPO }}
FORCE_UPDATE: ${{ github.event.inputs.force_update || 'false' }}
BLOG_FILES: ${{ steps.detect-files.outputs.files }}
run: |
node << 'EOF'
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');

const BASEHUB_API = 'https://api.basehub.com/graphql';
const token = process.env.BASEHUB_TOKEN;
const repo = process.env.BASEHUB_REPO;
const forceUpdate = process.env.FORCE_UPDATE === 'true';
const blogFiles = process.env.BLOG_FILES.trim().split(/\s+/).filter(Boolean);

if (!token || !repo) {
console.log('⚠️ BASEHUB_TOKEN or BASEHUB_REPO not configured. Skipping sync.');
console.log(' To enable Basehub sync:');
console.log(' 1. Get your admin token from basehub.com → Settings → API Tokens');
console.log(' 2. Add BASEHUB_TOKEN secret to this repository');
console.log(' 3. Add BASEHUB_REPO secret (format: "org/repo")');
process.exit(0);
}

async function graphqlRequest(query, variables = {}) {
const response = await fetch(BASEHUB_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'X-Basehub-Repo': repo
},
body: JSON.stringify({ query, variables })
});

const result = await response.json();
if (result.errors) {
throw new Error(JSON.stringify(result.errors, null, 2));
}
return result.data;
}

async function createDraftPost(frontmatter, content, filePath) {
const slug = path.basename(filePath, path.extname(filePath));

// Basehub mutation to create a draft blog post
// Adjust the mutation based on your Basehub schema
const mutation = `
mutation CreateBlogPost($input: CreatePostInput!) {
createPost(input: $input) {
_id
_slug
_title
}
}
`;

// Alternative: Use transactionAsync for complex operations
const transactionMutation = `
mutation CreateDraftBlog($operations: [TransactionOperation!]!) {
transactionAsync(operations: $operations) {
id
status
}
}
`;

const input = {
_title: frontmatter.title || slug,
_slug: slug,
description: frontmatter.description || '',
date: frontmatter.date || new Date().toISOString().split('T')[0],
author: frontmatter.author || 'Venice Team',
tags: frontmatter.tags || [],
featured: frontmatter.featured || false,
body: content,
_status: 'draft' // Create as draft
};

console.log(`📝 Creating draft: "${input._title}"`);
console.log(` Slug: ${slug}`);
console.log(` Tags: ${input.tags.join(', ') || 'none'}`);

try {
// Try using the transaction API for more control
const operations = [{
type: 'create',
collection: 'posts', // Adjust to your collection name (posts, blog, articles, etc.)
data: input
}];

const result = await graphqlRequest(transactionMutation, { operations });
console.log(`✅ Draft created successfully!`);
console.log(` Transaction ID: ${result.transactionAsync?.id || 'N/A'}`);
return result;
} catch (error) {
// If transaction fails, try simpler approach
console.log(`⚠️ Transaction API not available, trying direct creation...`);

try {
const result = await graphqlRequest(mutation, { input });
console.log(`✅ Draft created: ${result.createPost?._id || 'success'}`);
return result;
} catch (innerError) {
console.error(`❌ Failed to create draft: ${innerError.message}`);
throw innerError;
}
}
}

async function processFile(filePath) {
if (!fs.existsSync(filePath)) {
console.log(`⚠️ File not found: ${filePath}`);
return;
}

const fileContent = fs.readFileSync(filePath, 'utf-8');
const { data: frontmatter, content } = matter(fileContent);

console.log(`\n📄 Processing: ${filePath}`);

await createDraftPost(frontmatter, content, filePath);
}

async function main() {
console.log('🚀 Basehub Blog Sync');
console.log(` Repository: ${repo}`);
console.log(` Files to process: ${blogFiles.length}`);
console.log(` Force update: ${forceUpdate}`);
console.log('');

for (const file of blogFiles) {
try {
await processFile(file);
} catch (error) {
console.error(`\n❌ Error processing ${file}:`);
console.error(error.message);
// Continue with other files
}
}

console.log('\n✨ Sync complete!');
console.log(' View drafts at: https://basehub.com/' + repo);
}

main().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});
EOF

- name: Summary
run: |
echo "## Blog to Basehub Sync Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Files processed:** ${{ steps.detect-files.outputs.files || 'None' }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Next Steps" >> $GITHUB_STEP_SUMMARY
echo "1. Go to [Basehub CMS](https://basehub.com) to review the draft" >> $GITHUB_STEP_SUMMARY
echo "2. Edit and polish the content if needed" >> $GITHUB_STEP_SUMMARY
echo "3. Publish when ready" >> $GITHUB_STEP_SUMMARY
77 changes: 77 additions & 0 deletions blog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Blog Posts

This directory contains blog posts that can be automatically synced to Basehub CMS as drafts.

## File Format

Blog posts should be MDX files with YAML frontmatter:

```mdx
---
title: "Your Blog Post Title"
description: "A brief description for SEO and previews"
date: "2026-01-25"
author: "Venice Team"
tags:
- tutorial
- privacy
- api
image: null
featured: false
---

# Your Blog Post Title

Content goes here...
```

### Frontmatter Fields

| Field | Required | Description |
|-------|----------|-------------|
| `title` | Yes | The blog post title |
| `description` | Yes | Short description for SEO/previews |
| `date` | Yes | Publication date (YYYY-MM-DD format) |
| `author` | No | Author name (defaults to "Venice Team") |
| `tags` | No | Array of tags for categorization |
| `image` | No | Cover image URL |
| `featured` | No | Whether to feature this post (default: false) |

## Automatic Sync to Basehub

When blog posts are pushed to the `main` branch, the GitHub Actions workflow automatically:

1. Detects new or changed `.mdx` files in this directory
2. Parses the frontmatter and content
3. Creates a **draft** post in Basehub CMS
4. Notifies you in the workflow summary

### Setup Requirements

To enable Basehub sync, add these secrets to your repository:

1. **`BASEHUB_TOKEN`**: Your Basehub admin API token
- Get it from: basehub.com → Your Repo → Settings → API Tokens

2. **`BASEHUB_REPO`**: Your Basehub repository identifier
- Format: `your-org/your-repo`

### Manual Sync

You can also manually trigger a sync from the Actions tab:

1. Go to Actions → "Blog to Basehub CMS"
2. Click "Run workflow"
3. Optionally specify a specific file path
4. Check "Force update" to overwrite existing drafts

## Workflow

1. **Write**: Create your `.mdx` file in this directory
2. **Commit**: Push to main (or open a PR)
3. **Review**: Check the draft in Basehub CMS
4. **Publish**: Edit and publish from Basehub when ready

## Current Posts

- `venice-api-clawdbot-tutorial.mdx` - How to use Venice API with Clawdbot
Loading