Skip to content
Closed
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
258 changes: 258 additions & 0 deletions .github/scripts/sync-openapi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
#!/usr/bin/env node

/**
* Sync OpenAPI Specification from Hedera Mirror Node
*
* This script downloads the latest OpenAPI specification from Hedera Mirror Node
* and updates the local openapi.yaml file if there are changes.
*
* Usage:
* node sync-openapi.js [--network=testnet|mainnet] [--output=path/to/openapi.yaml]
*/

const https = require('https');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

// Configuration
const NETWORKS = {
testnet: 'https://testnet.mirrornode.hedera.com/api/v1/docs/openapi.yml',
mainnet: 'https://mainnet.mirrornode.hedera.com/api/v1/docs/openapi.yml'
};

// Parse command line arguments
const args = process.argv.slice(2).reduce((acc, arg) => {
const [key, value] = arg.replace('--', '').split('=');
acc[key] = value || true;
return acc;
}, {});

const network = args.network || 'mainnet';
const outputPath = args.output || path.join(process.cwd(), 'openapi.yaml');
const dryRun = args['dry-run'] || false;
const verbose = args.verbose || false;

// Validate network
if (!NETWORKS[network]) {
console.error(`❌ Error: Invalid network "${network}". Must be "testnet" or "mainnet".`);
process.exit(1);
}

const sourceUrl = NETWORKS[network];

/**
* Log message if verbose mode is enabled
*/
function log(message) {
if (verbose) {
console.log(message);
}
}

/**
* Download content from URL
*/
function downloadFile(url) {
return new Promise((resolve, reject) => {
log(`📥 Downloading from ${url}...`);

https.get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
return;
}

let data = '';
response.on('data', (chunk) => {
data += chunk;
});

response.on('end', () => {
log(`✓ Download complete (${data.length} bytes)`);
resolve(data);
});
}).on('error', (error) => {
reject(error);
});
});
}

/**
* Extract version from OpenAPI content
*/
function extractVersion(content) {
const match = content.match(/version:\s*['"]?([0-9.]+)['"]?/);
return match ? match[1] : 'unknown';
}

/**
* Check if files are different
*/
function filesAreDifferent(content1, content2) {
// Normalize line endings and trim whitespace
const normalize = (str) => str.replace(/\r\n/g, '\n').trim();
return normalize(content1) !== normalize(content2);
}

/**
* Create backup of existing file
*/
function createBackup(filePath) {
if (fs.existsSync(filePath)) {
const backupPath = `${filePath}.backup`;
fs.copyFileSync(filePath, backupPath);
log(`✓ Created backup at ${backupPath}`);
return backupPath;
}
return null;
}

/**
* Validate YAML syntax
*/
function validateYaml(content) {
try {
// Try to parse with Node.js (basic validation)
// For production, consider using a proper YAML parser like 'js-yaml'
const lines = content.split('\n');
let indentStack = [0];

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '' || line.trim().startsWith('#')) continue;

const indent = line.search(/\S/);
if (indent === -1) continue;

// Basic indentation check
if (indent > indentStack[indentStack.length - 1]) {
indentStack.push(indent);
} else {
while (indentStack.length > 0 && indent < indentStack[indentStack.length - 1]) {
indentStack.pop();
}
}
}

log('✓ YAML syntax validation passed');
return true;
} catch (error) {
console.error(`❌ YAML validation failed: ${error.message}`);
return false;
}
}

/**
* Main sync function
*/
async function syncOpenApi() {
console.log('🔄 Hedera Mirror Node OpenAPI Sync');
console.log('=====================================');
console.log(`Network: ${network}`);
console.log(`Source: ${sourceUrl}`);
console.log(`Output: ${outputPath}`);
console.log(`Dry run: ${dryRun ? 'Yes' : 'No'}`);
console.log('');

try {
// Download new content
const newContent = await downloadFile(sourceUrl);
const newVersion = extractVersion(newContent);

console.log(`📦 Downloaded version: ${newVersion}`);

// Validate YAML
if (!validateYaml(newContent)) {
throw new Error('Downloaded content failed YAML validation');
}

// Check if file exists and compare
let hasChanges = true;
let currentVersion = 'none';

if (fs.existsSync(outputPath)) {
const currentContent = fs.readFileSync(outputPath, 'utf8');
currentVersion = extractVersion(currentContent);
hasChanges = filesAreDifferent(currentContent, newContent);

console.log(`📄 Current version: ${currentVersion}`);

if (!hasChanges) {
console.log('✅ No changes detected. File is up to date.');
return { updated: false, version: currentVersion };
}

console.log('🔍 Changes detected!');
} else {
console.log('📝 No existing file found. Creating new file.');
}

if (dryRun) {
console.log('');
console.log('🔍 DRY RUN MODE - No files will be modified');
console.log(`Would update from version ${currentVersion} to ${newVersion}`);
return { updated: false, version: newVersion, dryRun: true };
}

// Create backup
const backupPath = createBackup(outputPath);

// Write new content
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}

fs.writeFileSync(outputPath, newContent, 'utf8');
console.log(`✅ Successfully updated ${outputPath}`);

if (backupPath) {
console.log(`💾 Backup saved to ${backupPath}`);
}

// Summary
console.log('');
console.log('📊 Summary:');
console.log(` Previous version: ${currentVersion}`);
console.log(` New version: ${newVersion}`);
console.log(` File size: ${newContent.length} bytes`);

return {
updated: true,
version: newVersion,
previousVersion: currentVersion,
backupPath
};

} catch (error) {
console.error('');
console.error('❌ Error:', error.message);

if (verbose && error.stack) {
console.error('');
console.error('Stack trace:');
console.error(error.stack);
}

process.exit(1);
}
}

// Run if called directly
if (require.main === module) {
syncOpenApi()
.then((result) => {
if (result.updated) {
process.exit(0);
} else {
process.exit(0);
}
})
.catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
}

module.exports = { syncOpenApi, NETWORKS };
131 changes: 131 additions & 0 deletions .github/workflows/sync-openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Sync OpenAPI Specification

on:
# Run daily at 2 AM UTC to check for updates
schedule:
- cron: '0 2 * * *'

# Allow manual trigger
workflow_dispatch:
inputs:
network:
description: 'Network to sync from'
required: true
default: 'testnet'
type: choice
options:
- mainnet
- testnet
create_pr:
description: 'Create pull request if changes detected'
required: true
default: true
type: boolean

jobs:
sync-openapi:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

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

- name: Run OpenAPI sync script
id: sync
run: |
NETWORK="${{ github.event.inputs.network || 'mainnet' }}"
echo "Syncing from $NETWORK network..."

# Run the sync script
node sync-openapi.js --network=$NETWORK --output=openapi.yaml --verbose

# Check if there are changes
if git diff --quiet openapi.yaml; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No changes detected"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "Changes detected"

# Extract version from the new file
VERSION=$(grep -m 1 "version:" openapi.yaml | sed 's/.*version: *//;s/["\x27]//g')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Detected version: $VERSION"
fi

- name: Create Pull Request
if: steps.sync.outputs.has_changes == 'true' && (github.event.inputs.create_pr != 'false' || github.event_name == 'schedule')
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: update OpenAPI spec to v${{ steps.sync.outputs.version }}'
title: 'Update Mirror Node OpenAPI Specification to v${{ steps.sync.outputs.version }}'
body: |
## 🔄 Automated OpenAPI Sync

This PR updates the OpenAPI specification from the Hedera Mirror Node API.

**Details:**
- **Network:** ${{ github.event.inputs.network || 'mainnet' }}
- **Version:** ${{ steps.sync.outputs.version }}
- **Source:** https://${{ github.event.inputs.network || 'mainnet' }}.mirrornode.hedera.com/api/v1/docs/openapi.yml
- **Triggered by:** ${{ github.event_name }}

**Changes:**
This automated sync detected changes in the Mirror Node OpenAPI specification. Please review the changes to ensure they align with the documentation requirements.

**Review Checklist:**
- [ ] Verify new endpoints are documented
- [ ] Check for breaking changes
- [ ] Update related documentation if needed
- [ ] Validate YAML syntax

---

🤖 This PR was automatically created by the OpenAPI sync workflow.

**Related Links:**
- [Mirror Node Release Notes](https://docs.hedera.com/hedera/networks/release-notes/mirror-node)
- [Mirror Node API Docs](https://docs.hedera.com/hedera/sdks-and-apis/rest-api)
branch: automated/sync-openapi-${{ steps.sync.outputs.version }}
delete-branch: true
labels: |
automated
documentation
mirror-node
openapi

- name: Commit changes directly (if no PR)
if: steps.sync.outputs.has_changes == 'true' && github.event.inputs.create_pr == 'false' && github.event_name != 'schedule'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add openapi.yaml
git commit -m "chore: update OpenAPI spec to v${{ steps.sync.outputs.version }}"
git push

- name: Summary
run: |
echo "## OpenAPI Sync Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Network:** ${{ github.event.inputs.network || 'mainnet' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Has Changes:** ${{ steps.sync.outputs.has_changes }}" >> $GITHUB_STEP_SUMMARY

if [ "${{ steps.sync.outputs.has_changes }}" == "true" ]; then
echo "- **New Version:** ${{ steps.sync.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ OpenAPI specification has been updated." >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "ℹ️ No changes detected. OpenAPI specification is up to date." >> $GITHUB_STEP_SUMMARY
fi
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
title: "Add Hedera to MetaMask"

Check warning on line 2 in hedera/getting-started-evm-developers/add-hedera-to-metamask.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hedera-0c6e0218) - vale-spellcheck

hedera/getting-started-evm-developers/add-hedera-to-metamask.mdx#L2

Did you really mean 'Hedera'?
---


Hedera is fully compatible with web3 wallets like MetaMask. Just add the JSON-RPC endpoint as a custom network.

Check warning on line 6 in hedera/getting-started-evm-developers/add-hedera-to-metamask.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hedera-0c6e0218) - vale-spellcheck

hedera/getting-started-evm-developers/add-hedera-to-metamask.mdx#L6

Did you really mean 'Hedera'?

## Mainnet

Check warning on line 8 in hedera/getting-started-evm-developers/add-hedera-to-metamask.mdx

View check run for this annotation

Mintlify / Mintlify Validation (hedera-0c6e0218) - vale-spellcheck

hedera/getting-started-evm-developers/add-hedera-to-metamask.mdx#L8

Did you really mean 'Mainnet'?

<Card
title="ADD HEDERA MAINNET"
Expand All @@ -26,3 +26,6 @@
/>

<table><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody><tr><td><strong>Network Name</strong></td><td>Hedera Testnet</td></tr><tr><td><strong>RPC Endpoint</strong></td><td><code>https://testnet.hashio.io/api</code></td></tr><tr><td><strong>Chain ID</strong></td><td><code>296</code></td></tr><tr><td><strong>Currency Symbol</strong></td><td>HBAR</td></tr><tr><td><strong>Block Explorer URL</strong></td><td><code>https://hashscan.io/testnet</code></td></tr></tbody></table>


# TESTING
Loading