Skip to content

Commit e87252e

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

File tree

3 files changed

+172
-0
lines changed

3 files changed

+172
-0
lines changed

.github/scripts/sync-changes.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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.error(`Could not read ${file}:`, error.message);
45+
process.exit(1);
46+
}
47+
}
48+
49+
// Get deleted files from HEAD commit
50+
const deletedFiles = execSync('git diff --name-only --diff-filter=D HEAD~1 HEAD', { encoding: 'utf8' })
51+
.trim().split('\n').filter(Boolean)
52+
.filter(path => { return !path.startsWith('.') && path.includes('/'); });
53+
54+
for (const file of deletedFiles) {
55+
updates[file] = null; // null indicates deletion
56+
}
57+
58+
return updates;
59+
}
60+
61+
// Helper function to create PUT options for a file
62+
function createPutOptions(file, content) {
63+
const path = require('path').dirname(file);
64+
const filename = require('path').basename(file, require('path').extname(file));
65+
const url = `https://discourse.julialang.org/admin/customize/${path}/${filename}`;
66+
67+
const { URLSearchParams } = require('url');
68+
const params = new URLSearchParams();
69+
params.append('site_text[value]', content);
70+
params.append('site_text[locale]', 'en');
71+
const payload = params.toString();
72+
73+
const urlObj = new URL(url);
74+
return {
75+
url,
76+
options: {
77+
hostname: urlObj.hostname,
78+
path: urlObj.pathname,
79+
method: 'PUT',
80+
headers: {
81+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
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+
process.exit(1);
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+
if (res.statusCode >= 200 && res.statusCode < 300) {
117+
console.log(`✅ Updated ${file}${url} (HTTP ${res.statusCode})`);
118+
} else {
119+
console.error(`❌ Failed to update ${file}: HTTP ${res.statusCode}`);
120+
process.exit(1);
121+
}
122+
});
123+
124+
req.on('error', (error) => {
125+
console.error(`❌ Failed to update ${file}: ${error.message}`);
126+
process.exit(1);
127+
});
128+
129+
req.write(payload);
130+
req.end();
131+
}
132+
}
133+
}
134+
135+
// Main execution
136+
console.log(`🚀 Running in ${DEPLOY ? 'LIVE' : 'DRY RUN'} mode`);
137+
138+
validateSingleCommit();
139+
const updates = getFileChanges();
140+
sendUpdates(updates);

.github/workflows/sync-changes.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
environment: "GitHub Actions CI"
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0 # Fetch full history for PR validation
18+
19+
- name: Setup Node.js
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: '18'
23+
24+
- name: Run sync script
25+
run: node .github/scripts/sync-changes.js
26+
env:
27+
API_KEY: ${{ secrets.API_KEY }}
28+
API_USER: ${{ secrets.API_USER }}
29+
GITHUB_EVENT_NAME: ${{ github.event_name }}
30+
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
31+
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 https://github.com/JuliaDiscourse/SiteTexts.

0 commit comments

Comments
 (0)