Skip to content

Sync Modrinth Releases to GitHub #92

Sync Modrinth Releases to GitHub

Sync Modrinth Releases to GitHub #92

Workflow file for this run

name: Sync Modrinth Releases to GitHub
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
sync-to-github:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Create sync script
run: |
cat > sync-modrinth.js << 'EOF'
const fs = require('fs');
const https = require('https');
const path = require('path');
const { execSync } = require('child_process');
// Constants
const MODRINTH_PROJECT_ID = process.env.MODRINTH_PROJECT_ID;
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const GITHUB_REPO = process.env.GITHUB_REPOSITORY;
// Get existing GitHub releases
function getGitHubReleases() {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: `/repos/${GITHUB_REPO}/releases`,
method: 'GET',
headers: {
'User-Agent': 'ModrinthSync',
'Authorization': `token ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json'
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(JSON.parse(data));
} else {
reject(new Error(`Status Code: ${res.statusCode}, Body: ${data}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.end();
});
}
// Get Modrinth versions
function getModrinthVersions() {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.modrinth.com',
path: `/v2/project/${MODRINTH_PROJECT_ID}/version`,
method: 'GET',
headers: {
'User-Agent': 'ModrinthSync/1.0'
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(JSON.parse(data));
} else {
reject(new Error(`Status Code: ${res.statusCode}, Body: ${data}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.end();
});
}
// Download file from URL
function downloadFile(url, destination) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(destination);
https.get(url, (response) => {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', (err) => {
fs.unlink(destination);
reject(err);
});
});
}
// Create GitHub release
function createGitHubRelease(version) {
return new Promise(async (resolve, reject) => {
try {
// Download the file first
const fileName = path.basename(version.files[0].url);
const filePath = `./${fileName}`;
console.log(`Downloading file from ${version.files[0].url} to ${filePath}`);
await downloadFile(version.files[0].url, filePath);
// Create tag if it doesn't exist
try {
console.log(`Checking if tag ${version.version_number} exists...`);
execSync(`git ls-remote --tags origin refs/tags/${version.version_number}`);
console.log(`Tag ${version.version_number} exists.`);
} catch (err) {
console.log(`Creating tag ${version.version_number}...`);
execSync(`git tag ${version.version_number}`);
execSync(`git push origin ${version.version_number}`);
console.log(`Tag ${version.version_number} created.`);
}
// Prepare release data
const isPrerelease = version.version_type === 'alpha' || version.version_type === 'beta';
const releaseData = {
tag_name: version.version_number,
name: version.name || `Version ${version.version_number}`,
body: `${version.changelog || 'No changelog provided.'}`,
draft: false,
prerelease: isPrerelease
};
// Create the release
const options = {
hostname: 'api.github.com',
path: `/repos/${GITHUB_REPO}/releases`,
method: 'POST',
headers: {
'User-Agent': 'ModrinthSync',
'Authorization': `token ${GITHUB_TOKEN}`,
'Content-Type': 'application/json',
'Accept': 'application/vnd.github.v3+json'
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
const release = JSON.parse(data);
// Upload the asset
uploadAsset(release.upload_url.replace(/{.*}/, ''), filePath, fileName)
.then(() => resolve(release))
.catch(reject);
} else {
reject(new Error(`Status Code: ${res.statusCode}, Body: ${data}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.write(JSON.stringify(releaseData));
req.end();
} catch (error) {
reject(error);
}
});
}
// Upload asset to GitHub release
function uploadAsset(uploadUrl, filePath, fileName) {
return new Promise((resolve, reject) => {
const stats = fs.statSync(filePath);
const fileSize = stats.size;
const fileStream = fs.createReadStream(filePath);
const options = {
hostname: new URL(uploadUrl).hostname,
path: `${new URL(uploadUrl).pathname}?name=${encodeURIComponent(fileName)}`,
method: 'POST',
headers: {
'User-Agent': 'ModrinthSync',
'Authorization': `token ${GITHUB_TOKEN}`,
'Content-Type': 'application/java-archive',
'Content-Length': fileSize,
'Accept': 'application/vnd.github.v3+json'
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
fs.unlinkSync(filePath); // Clean up
resolve(JSON.parse(data));
} else {
reject(new Error(`Status Code: ${res.statusCode}, Body: ${data}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
fileStream.pipe(req);
});
}
// Main function
async function syncModrinthToGitHub() {
try {
console.log('Starting Modrinth to GitHub sync...');
// Get existing GitHub releases
const githubReleases = await getGitHubReleases();
const existingTags = githubReleases.map(release => release.tag_name);
console.log(`Found ${existingTags.length} existing GitHub releases.`);
// Get Modrinth versions
const modrinthVersions = await getModrinthVersions();
console.log(`Found ${modrinthVersions.length} versions on Modrinth.`);
// Find versions that don't exist on GitHub and sort by date (oldest first)
const newVersions = modrinthVersions
.filter(version => !existingTags.includes(version.version_number))
.sort((a, b) => new Date(a.date_published) - new Date(b.date_published));
console.log(`Found ${newVersions.length} new versions to sync to GitHub.`);
// Create GitHub releases for new versions
for (const version of newVersions) {
try {
console.log(`Creating GitHub release for version ${version.version_number} (published ${new Date(version.date_published).toISOString()})...`);
await createGitHubRelease(version);
console.log(`Successfully created GitHub release for version ${version.version_number}.`);
} catch (err) {
console.error(`Error creating release for ${version.version_number}:`, err.message);
}
}
console.log('Sync completed.');
} catch (error) {
console.error('Sync failed:', error.message);
process.exit(1);
}
}
// Execute
syncModrinthToGitHub();
EOF
- name: Run sync script
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MODRINTH_PROJECT_ID: "9tQwxSFr"
run: node sync-modrinth.js