229 lines
6.5 KiB
JavaScript
229 lines
6.5 KiB
JavaScript
// Redmine REST API utility
|
|
// Usage: node deploy/redmine-api.mjs <command> [args...]
|
|
|
|
// Allow self-signed / mismatched certs when calling via IP
|
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
|
|
const BASE_URL = 'https://81.200.150.204';
|
|
const API_KEY = 'fca900011c01bf5c83862a5107c4d798fb6a4ef8';
|
|
|
|
async function request(method, path, body) {
|
|
const url = `${BASE_URL}${path}`;
|
|
const opts = {
|
|
method,
|
|
headers: {
|
|
'X-Redmine-API-Key': API_KEY,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
};
|
|
if (body) opts.body = JSON.stringify(body);
|
|
|
|
const res = await fetch(url, opts);
|
|
const text = await res.text();
|
|
if (!res.ok) {
|
|
throw new Error(`${res.status} ${res.statusText}: ${text}`);
|
|
}
|
|
return text ? JSON.parse(text) : null;
|
|
}
|
|
|
|
// --- Projects ---
|
|
|
|
export async function listProjects() {
|
|
const data = await request('GET', '/projects.json');
|
|
return data.projects;
|
|
}
|
|
|
|
export async function getProject(idOrIdentifier) {
|
|
const data = await request('GET', `/projects/${idOrIdentifier}.json`);
|
|
return data.project;
|
|
}
|
|
|
|
export async function createProject(name, identifier, description = '') {
|
|
const data = await request('POST', '/projects.json', {
|
|
project: { name, identifier, description },
|
|
});
|
|
return data.project;
|
|
}
|
|
|
|
// --- Issues ---
|
|
|
|
export async function listIssues(projectIdentifier, params = {}) {
|
|
const query = new URLSearchParams(params);
|
|
if (projectIdentifier) query.set('project_id', projectIdentifier);
|
|
const data = await request('GET', `/issues.json?${query}`);
|
|
return data.issues;
|
|
}
|
|
|
|
export async function getIssue(id) {
|
|
const data = await request('GET', `/issues/${id}.json`);
|
|
return data.issue;
|
|
}
|
|
|
|
export async function createIssue(projectIdentifier, subject, description = '', extras = {}) {
|
|
const data = await request('POST', '/issues.json', {
|
|
issue: {
|
|
project_id: projectIdentifier,
|
|
subject,
|
|
description,
|
|
...extras,
|
|
},
|
|
});
|
|
return data.issue;
|
|
}
|
|
|
|
export async function updateIssue(id, fields) {
|
|
await request('PUT', `/issues/${id}.json`, { issue: fields });
|
|
return { id, ...fields };
|
|
}
|
|
|
|
export async function deleteIssue(id) {
|
|
await request('DELETE', `/issues/${id}.json`);
|
|
return { id, deleted: true };
|
|
}
|
|
|
|
// --- Wiki ---
|
|
|
|
export async function listWikiPages(projectId) {
|
|
const data = await request('GET', `/projects/${projectId}/wiki/index.json`);
|
|
return data.wiki_pages;
|
|
}
|
|
|
|
export async function getWikiPage(projectId, title) {
|
|
const data = await request('GET', `/projects/${projectId}/wiki/${encodeURIComponent(title)}.json`);
|
|
return data.wiki_page;
|
|
}
|
|
|
|
export async function putWikiPage(projectId, title, text, comments = '') {
|
|
const body = { wiki_page: { text } };
|
|
if (comments) body.wiki_page.comments = comments;
|
|
await request('PUT', `/projects/${projectId}/wiki/${encodeURIComponent(title)}.json`, body);
|
|
return { project: projectId, title, updated: true };
|
|
}
|
|
|
|
export async function deleteWikiPage(projectId, title) {
|
|
await request('DELETE', `/projects/${projectId}/wiki/${encodeURIComponent(title)}.json`);
|
|
return { project: projectId, title, deleted: true };
|
|
}
|
|
|
|
// --- Trackers, Statuses, Priorities (reference data) ---
|
|
|
|
export async function listTrackers() {
|
|
const data = await request('GET', '/trackers.json');
|
|
return data.trackers;
|
|
}
|
|
|
|
export async function listStatuses() {
|
|
const data = await request('GET', '/issue_statuses.json');
|
|
return data.issue_statuses;
|
|
}
|
|
|
|
// --- CLI ---
|
|
|
|
const commands = {
|
|
'list-projects': async () => {
|
|
const projects = await listProjects();
|
|
console.table(projects.map(p => ({ id: p.id, name: p.name, identifier: p.identifier })));
|
|
},
|
|
|
|
'get-project': async ([id]) => {
|
|
const project = await getProject(id);
|
|
console.log(JSON.stringify(project, null, 2));
|
|
},
|
|
|
|
'create-project': async ([name, identifier, description]) => {
|
|
const project = await createProject(name, identifier, description);
|
|
console.log('Created project:', project.id, project.identifier);
|
|
},
|
|
|
|
'list-issues': async ([project, ...rest]) => {
|
|
const params = {};
|
|
for (const arg of rest) {
|
|
const [k, v] = arg.split('=');
|
|
params[k] = v;
|
|
}
|
|
const issues = await listIssues(project, params);
|
|
console.table(issues.map(i => ({
|
|
id: i.id,
|
|
tracker: i.tracker?.name,
|
|
status: i.status?.name,
|
|
subject: i.subject,
|
|
})));
|
|
},
|
|
|
|
'get-issue': async ([id]) => {
|
|
const issue = await getIssue(id);
|
|
console.log(JSON.stringify(issue, null, 2));
|
|
},
|
|
|
|
'create-issue': async ([project, subject, description]) => {
|
|
const issue = await createIssue(project, subject, description);
|
|
console.log('Created issue:', issue.id, '-', issue.subject);
|
|
},
|
|
|
|
'update-issue': async ([id, ...fields]) => {
|
|
const data = {};
|
|
for (const f of fields) {
|
|
const [k, v] = f.split('=');
|
|
data[k] = v;
|
|
}
|
|
const result = await updateIssue(id, data);
|
|
console.log('Updated issue:', result.id);
|
|
},
|
|
|
|
'delete-issue': async ([id]) => {
|
|
await deleteIssue(id);
|
|
console.log('Deleted issue:', id);
|
|
},
|
|
|
|
'list-trackers': async () => {
|
|
const trackers = await listTrackers();
|
|
console.table(trackers.map(t => ({ id: t.id, name: t.name })));
|
|
},
|
|
|
|
'list-statuses': async () => {
|
|
const statuses = await listStatuses();
|
|
console.table(statuses.map(s => ({ id: s.id, name: s.name })));
|
|
},
|
|
|
|
'list-wiki': async ([project]) => {
|
|
const pages = await listWikiPages(project);
|
|
console.table(pages.map(p => ({ title: p.title, version: p.version, created: p.created_on })));
|
|
},
|
|
|
|
'get-wiki': async ([project, title]) => {
|
|
const page = await getWikiPage(project, title);
|
|
console.log(`# ${page.title} (v${page.version})\n`);
|
|
console.log(page.text);
|
|
},
|
|
|
|
'put-wiki': async ([project, title, ...textParts]) => {
|
|
const text = textParts.join(' ');
|
|
const result = await putWikiPage(project, title, text, 'Updated via CLI');
|
|
console.log('Updated wiki page:', result.title);
|
|
},
|
|
|
|
'delete-wiki': async ([project, title]) => {
|
|
await deleteWikiPage(project, title);
|
|
console.log('Deleted wiki page:', title);
|
|
},
|
|
};
|
|
|
|
// Only run CLI when this file is executed directly
|
|
const isMain = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/').replace(/^.*:/, ''));
|
|
if (isMain) {
|
|
const [command, ...args] = process.argv.slice(2);
|
|
|
|
if (!command || !commands[command]) {
|
|
console.log('Usage: node deploy/redmine-api.mjs <command> [args...]');
|
|
console.log('Commands:', Object.keys(commands).join(', '));
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
await commands[command](args);
|
|
} catch (e) {
|
|
console.error('Error:', e.message);
|
|
process.exit(1);
|
|
}
|
|
}
|