Skip to content

Commit 25d3adb

Browse files
mbaumanclaude
andauthored
wip: setup infra to set discourse customizations via API (#1)
Co-authored-by: Claude <[email protected]>
1 parent f5683bd commit 25d3adb

File tree

3 files changed

+165
-0
lines changed

3 files changed

+165
-0
lines changed

.github/scripts/sync-changes.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env node
2+
3+
const { execSync } = require('child_process');
4+
const { readFileSync } = require('fs');
5+
const https = require('https');
6+
7+
// Configuration
8+
const API_KEY = process.env.API_KEY;
9+
const API_USER = process.env.API_USER;
10+
const EVENT_NAME = process.env.GITHUB_EVENT_NAME;
11+
const PR_BASE_SHA = process.env.PR_BASE_SHA;
12+
const PR_HEAD_SHA = process.env.PR_HEAD_SHA;
13+
14+
const DEPLOY = EVENT_NAME === 'push';
15+
16+
// Validate PR has only one commit
17+
function validateSingleCommit() {
18+
if (DEPLOY) return; // GitHub branch protections disallow merge commits
19+
20+
const commitCount = execSync(`git rev-list --count ${PR_BASE_SHA}..${PR_HEAD_SHA}`, { encoding: 'utf8' }).trim();
21+
22+
if (parseInt(commitCount) !== 1) {
23+
console.error(`❌ PR must contain exactly 1 commit, found ${commitCount}`);
24+
console.error('Please squash your commits before merging');
25+
process.exit(1);
26+
}
27+
28+
console.log('✅ PR contains exactly 1 commit');
29+
}
30+
31+
// Get file changes from the single commit
32+
function getFileChanges() {
33+
const updates = {};
34+
35+
// Get added/modified files from HEAD commit
36+
const changedFiles = execSync('git diff --name-only --diff-filter=AM HEAD~1 HEAD', { encoding: 'utf8' })
37+
.trim().split('\n').filter(Boolean)
38+
.filter(path => { return !path.startsWith('.') && path.includes('/'); });
39+
40+
for (const file of changedFiles) {
41+
try {
42+
updates[file] = readFileSync(file, 'utf8');
43+
} catch (error) {
44+
console.warn(`Could not read ${file}:`, error.message);
45+
}
46+
}
47+
48+
// Get deleted files from HEAD commit
49+
const deletedFiles = execSync('git diff --name-only --diff-filter=D HEAD~1 HEAD', { encoding: 'utf8' })
50+
.trim().split('\n').filter(Boolean)
51+
.filter(path => { return !path.startsWith('.') && path.includes('/'); });
52+
53+
for (const file of deletedFiles) {
54+
updates[file] = null; // null indicates deletion
55+
}
56+
57+
return updates;
58+
}
59+
60+
// Helper function to create PUT options for a file
61+
function createPutOptions(file, content) {
62+
const path = require('path').dirname(file);
63+
const filename = require('path').basename(file, require('path').extname(file));
64+
const url = `https://discourse.julialang.org/admin/customize/${path}/${filename}`;
65+
66+
const { URLSearchParams } = require('url');
67+
const params = new URLSearchParams();
68+
params.append('site_text[value]', content);
69+
params.append('site_text[locale]', 'en');
70+
const payload = params.toString();
71+
72+
const urlObj = new URL(url);
73+
return {
74+
url,
75+
options: {
76+
hostname: urlObj.hostname,
77+
path: urlObj.pathname,
78+
method: 'PUT',
79+
headers: {
80+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
81+
'Content-Length': Buffer.byteLength(payload),
82+
'Accept': '*/*',
83+
'Api-Key': API_KEY,
84+
'Api-Username': API_USER
85+
}
86+
},
87+
payload
88+
};
89+
}
90+
91+
// Send updates to external API or print dry run
92+
function sendUpdates(updates) {
93+
const fileCount = Object.keys(updates).length;
94+
if (fileCount === 0) {
95+
console.log('No file changes detected');
96+
return;
97+
}
98+
console.log(`\n🔍 Sending ${fileCount} updates:`);
99+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
100+
101+
for (const [file, content] of Object.entries(updates)) {
102+
if (content === null) {
103+
console.error(`⚠️ Skipping deletion of ${file} (not supported)`);
104+
continue;
105+
}
106+
107+
const { url, options, payload } = createPutOptions(file, content);
108+
109+
console.log(`📝 UPDATE: ${file}${url}`);
110+
console.log(` PUT ${url}`);
111+
console.log(` Body: ${payload}\n`);
112+
if (DEPLOY) {
113+
// Actual API calls for main branch
114+
115+
const req = https.request(options, (res) => {
116+
console.log(`✅ Updated ${file}${url} (HTTP ${res.statusCode})`);
117+
});
118+
119+
req.on('error', (error) => {
120+
console.error(`❌ Failed to update ${file}: ${error.message}`);
121+
});
122+
123+
req.write(payload);
124+
req.end();
125+
}
126+
}
127+
}
128+
129+
// Main execution
130+
console.log(`🚀 Running in ${DEPLOY ? 'LIVE' : 'DRY RUN'} mode`);
131+
132+
validateSingleCommit();
133+
const updates = getFileChanges();
134+
sendUpdates(updates);

.github/workflows/sync-changes.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Sync File Changes
2+
3+
on:
4+
push:
5+
branches: [main] # Actual sync on main branch only
6+
pull_request: # Dry run on all PRs
7+
8+
jobs:
9+
sync-changes:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
with:
16+
fetch-depth: 0 # Fetch full history for PR validation
17+
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: '18'
22+
23+
- name: Run sync script
24+
run: node .github/scripts/sync-changes.js
25+
env:
26+
API_KEY: ${{ secrets.API_KEY }}
27+
API_USER: ${{ secrets.API_USER }}
28+
GITHUB_EVENT_NAME: ${{ github.event_name }}
29+
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
30+
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Customize any text used in Discourse to match your community’s voice and tone. **BUT NOT HERE!** Do this through the automated mechanism at [github.com/JuliaDiscourse/SiteTexts](https://github.com/JuliaDiscourse/SiteTexts).

0 commit comments

Comments
 (0)