Claude
Skills
Sign in
Back

notion-ci-integration

Included with Lifetime
$97 forever

Integrate the Notion API into CI/CD pipelines for automated documentation sync, deploy tracking, and configuration reads. Use when setting up GitHub Actions workflows that push release notes to Notion, update database entries on deploy, create incident pages from CI, or read feature flags from Notion databases. Trigger with phrases like "notion CI", "notion GitHub Actions", "notion deploy sync", "notion release notes automation", "notion CI pipeline".

Backend & APIssaasproductivitynotionci-cddevops

What this skill does

# Notion CI Integration

## Overview

Automate documentation sync, deploy tracking, and configuration management by integrating the Notion API into CI/CD pipelines. This skill covers GitHub Actions workflows that push changelogs and release notes to Notion pages, update database entries on successful deploys, create pages for incident reports, and read feature flags or configuration from Notion databases — all with proper rate limit handling for CI environments.

## Prerequisites

- GitHub repository with Actions enabled
- Notion internal integration token (create at `https://www.notion.so/my-integrations`)
- Target Notion pages/databases shared with the integration (click "..." > "Connections" > add your integration)
- `NOTION_TOKEN` stored as a GitHub Actions secret
- Node.js 18+ or Python 3.9+ in CI environment

## Instructions

### Step 1: GitHub Actions Workflow for Documentation Sync

Push changelogs and release notes to Notion automatically on release.

```yaml
# .github/workflows/notion-docs-sync.yml
name: Sync Docs to Notion

on:
  release:
    types: [published]
  push:
    branches: [main]
    paths: ['CHANGELOG.md', 'docs/**']

env:
  NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}

jobs:
  sync-release-notes:
    runs-on: ubuntu-latest
    if: github.event_name == 'release'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci

      - name: Push release notes to Notion
        run: node scripts/notion-release-sync.js
        env:
          NOTION_RELEASES_DB: ${{ secrets.NOTION_RELEASES_DB }}
          RELEASE_TAG: ${{ github.event.release.tag_name }}
          RELEASE_BODY: ${{ github.event.release.body }}
          RELEASE_URL: ${{ github.event.release.html_url }}

  sync-changelog:
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci

      - name: Sync CHANGELOG to Notion page
        run: node scripts/notion-changelog-sync.js
        env:
          NOTION_CHANGELOG_PAGE: ${{ secrets.NOTION_CHANGELOG_PAGE }}

  update-deploy-status:
    runs-on: ubuntu-latest
    needs: sync-release-notes
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci

      - name: Update deploy tracker in Notion
        run: node scripts/notion-deploy-update.js
        env:
          NOTION_DEPLOYS_DB: ${{ secrets.NOTION_DEPLOYS_DB }}
          DEPLOY_VERSION: ${{ github.event.release.tag_name }}
          DEPLOY_ENV: production
          DEPLOY_SHA: ${{ github.sha }}
```

### Step 2: CI Scripts for Notion Operations

#### Release Notes Sync (Node.js)

```typescript
// scripts/notion-release-sync.js
import { Client } from '@notionhq/client';

const notion = new Client({ auth: process.env.NOTION_TOKEN });
const databaseId = process.env.NOTION_RELEASES_DB;

async function syncReleaseNotes() {
  const tag = process.env.RELEASE_TAG;
  const body = process.env.RELEASE_BODY || 'No release notes provided.';
  const url = process.env.RELEASE_URL;

  // Create a new page in the releases database
  const page = await notion.pages.create({
    parent: { database_id: databaseId },
    properties: {
      Name: {
        title: [{ text: { content: `Release ${tag}` } }],
      },
      Version: {
        rich_text: [{ text: { content: tag } }],
      },
      Status: {
        select: { name: 'Released' },
      },
      'Release Date': {
        date: { start: new Date().toISOString().split('T')[0] },
      },
      'GitHub URL': {
        url: url,
      },
    },
  });

  // Append the release body as page content
  const blocks = body.split('\n').filter(Boolean).map((line) => ({
    paragraph: {
      rich_text: [{ text: { content: line } }],
    },
  }));

  // Notion API limits to 100 blocks per request
  for (let i = 0; i < blocks.length; i += 100) {
    await notion.blocks.children.append({
      block_id: page.id,
      children: blocks.slice(i, i + 100),
    });
    // Rate limit: wait between batch appends
    if (i + 100 < blocks.length) await sleep(350);
  }

  console.log(`Created release page: ${page.id}`);
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

syncReleaseNotes().catch((err) => {
  console.error('Failed to sync release notes:', err.message);
  process.exit(1);
});
```

#### Deploy Status Update (Node.js)

```typescript
// scripts/notion-deploy-update.js
import { Client } from '@notionhq/client';

const notion = new Client({ auth: process.env.NOTION_TOKEN });
const databaseId = process.env.NOTION_DEPLOYS_DB;

async function updateDeployStatus() {
  const version = process.env.DEPLOY_VERSION;
  const environment = process.env.DEPLOY_ENV || 'staging';
  const sha = process.env.DEPLOY_SHA;

  // Search for existing entry by version
  const existing = await notion.databases.query({
    database_id: databaseId,
    filter: {
      property: 'Version',
      rich_text: { equals: version },
    },
  });

  if (existing.results.length > 0) {
    // Update existing entry
    await notion.pages.update({
      page_id: existing.results[0].id,
      properties: {
        Status: { select: { name: 'Deployed' } },
        Environment: { select: { name: environment } },
        'Deploy Time': {
          date: { start: new Date().toISOString() },
        },
        'Commit SHA': {
          rich_text: [{ text: { content: sha.substring(0, 7) } }],
        },
      },
    });
    console.log(`Updated deploy entry for ${version}`);
  } else {
    // Create new deploy entry
    await notion.pages.create({
      parent: { database_id: databaseId },
      properties: {
        Name: {
          title: [{ text: { content: `Deploy ${version}` } }],
        },
        Version: {
          rich_text: [{ text: { content: version } }],
        },
        Status: { select: { name: 'Deployed' } },
        Environment: { select: { name: environment } },
        'Deploy Time': {
          date: { start: new Date().toISOString() },
        },
        'Commit SHA': {
          rich_text: [{ text: { content: sha.substring(0, 7) } }],
        },
      },
    });
    console.log(`Created deploy entry for ${version}`);
  }
}

updateDeployStatus().catch((err) => {
  console.error('Failed to update deploy status:', err.message);
  process.exit(1);
});
```

#### Python Batch Update Script for CI

```python
#!/usr/bin/env python3
# scripts/notion_batch_update.py
"""Batch update Notion database entries from CI.

Usage:
  python3 scripts/notion_batch_update.py --database-id <id> \
    --filter-property Status --filter-value "In Progress" \
    --set-property Status --set-value "Deployed" \
    --set-property Version --set-value "$TAG"
"""
import os
import sys
import time
import argparse
from notion_client import Client, APIResponseError

RATE_LIMIT_DELAY = 0.34  # 3 requests/sec max

def main():
    parser = argparse.ArgumentParser(description='Batch update Notion DB entries')
    parser.add_argument('--database-id', required=True)
    parser.add_argument('--filter-property', required=True)
    parser.add_argument('--filter-value', required=True)
    parser.add_argument('--set-property', action='append', required=True)
    parser.add_argument('--set-value', action='append', required=True)
    parser.add_argument('--dry-run', action='store_true')
    args = parser.parse_args()

    token = os.environ.get('NOTION_TOKEN')
    if not token:
        print('ERROR: NOTION_TOKEN not set', file=sys.stderr)
        sys.exit(1)

    notion = Client(auth=token)

    # Query with filter
    results = []
    cursor = None
    while True:
        response = notion.databases.query(
            database_id=args.database_id,
            filter={
                'property': args

Related in Backend & APIs