mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
Import -> GitHub -> Import a whole GitHub organization makes every repository of an organization or user its own board, opening in Board View / Version Control. Issues go through the ordinary importBoard, as the person who started the job, with their comments, labels and files. Releases are cards in a Releases list, their files attached only when chosen. Each .github/workflows file becomes Rules. The wiki is read from its git repository with isomorphic-git (1.42.4, pinned), every request through the SSRF guard and only to github.com: a page is a card in the Wiki list, its [[links]] point at and are attached to the linked pages' cards, and each commit that changed it is a dated edit in the card's history, so Restore brings back an older version. The job shows progress, can be cancelled, pauses on a restart and stops at a rate limit; the token is kept in memory only. Version Control gains Releases and Wiki tabs: the wiki as GitHub shows it, with the page list, _Sidebar and _Footer, links that stay in the tab, and each page's history with Restore. GitHub Actions workflows map to Rules: issue events to triggers, label, close, reopen, comment, assign and create steps to actions, actions/stale to an aging rule. Code - builds, tests, scripts - is never run: such a workflow is a disabled rule with its YAML as the description. WeKan's Rules gain what they lacked for it: a "commented on" trigger, an "add comment" action written as the person whose change ran the rule and only where they may comment, cron schedules in UTC, and a rule description shown on the rule. A disabled rule no longer fires on a schedule or from its button, which it did. The new strings are translated in every locale. Thanks to xet7 !
79 lines
4.3 KiB
JavaScript
79 lines
4.3 KiB
JavaScript
'use strict';
|
|
|
|
// The GitHub organization import (server/githubOrgImport.js): every
|
|
// repository of an organization - or of a user - as its own board, viewed in
|
|
// Board View / Version Control. Pure: tests/githubOrgImport.test.cjs.
|
|
//
|
|
// issues the issues with their comments, labels, milestones and files,
|
|
// through the ordinary GitHub importer (one board per repository)
|
|
// releases a card per release in the board's Releases list: the tag and
|
|
// name as the title, the notes as the description, the date as
|
|
// received, and - only when chosen - the release files attached
|
|
// actions each .github/workflows file as Rules
|
|
// (models/lib/githubActionsRules.js)
|
|
// wiki each wiki page as a card in the Wiki list (models/lib/githubWiki.js)
|
|
|
|
const PARTS = ['issues', 'releases', 'releaseFiles', 'actions', 'wiki'];
|
|
const DEFAULT_PARTS = ['issues', 'releases', 'actions', 'wiki'];
|
|
const RELEASES_LIST_TITLE = 'Releases';
|
|
const MAX_REPOS = 1000;
|
|
|
|
// GitHub's own rules: a login is 1-39 letters, digits and hyphens; a
|
|
// repository name 1-100 letters, digits, '.', '_' and '-'.
|
|
const OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
const REPO = /^[A-Za-z0-9._-]{1,100}$/;
|
|
|
|
/** The owner of `https://github.com/wekan`, `github.com/wekan/` or `wekan`. */
|
|
function ownerFrom(input) {
|
|
const text = String(input || '').trim().replace(/\/+$/, '');
|
|
const m = /^(?:https?:\/\/)?(?:www\.)?github\.com\/([^/?#]+)$/i.exec(text);
|
|
const owner = m ? m[1] : text;
|
|
return OWNER.test(owner) ? owner : null;
|
|
}
|
|
|
|
/** The import's options, cleaned, or throws with an i18n key. */
|
|
function cleanOrgImport(input) {
|
|
const owner = ownerFrom(input && input.owner);
|
|
if (!owner) throw new Error('github-org-import-owner-invalid');
|
|
const parts = Array.isArray(input.parts) ? [...new Set(input.parts.filter(p => PARTS.includes(p)))] : DEFAULT_PARTS.slice();
|
|
if (!parts.includes('issues')) parts.unshift('issues');
|
|
const repos = Array.isArray(input.repos) ? [...new Set(input.repos.map(String).filter(r => REPO.test(r)))] : null;
|
|
if (repos && !repos.length) throw new Error('github-org-import-no-repos');
|
|
return { owner, parts, repos, includeForks: input.includeForks === true, includeArchived: input.includeArchived !== false };
|
|
}
|
|
|
|
/** The repositories to import, in name order, from the API's repository list. */
|
|
function selectRepos(list, { owner, repos, includeForks, includeArchived }) {
|
|
const wanted = repos ? new Set(repos.map(r => r.toLowerCase())) : null;
|
|
return (Array.isArray(list) ? list : [])
|
|
.filter(r => r && typeof r.name === 'string' && REPO.test(r.name))
|
|
.filter(r => !r.owner || !r.owner.login || r.owner.login.toLowerCase() === owner.toLowerCase())
|
|
.filter(r => (wanted ? wanted.has(r.name.toLowerCase()) : (includeForks || !r.fork) && (includeArchived || !r.archived)))
|
|
.map(r => ({ name: r.name, description: r.description || '', archived: !!r.archived, fork: !!r.fork,
|
|
hasIssues: r.has_issues !== false, hasWiki: r.has_wiki !== false, private: !!r.private, url: r.html_url || '' }))
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
.slice(0, MAX_REPOS);
|
|
}
|
|
|
|
/** A release as a card: [{ title, description, receivedAt, assets }]. */
|
|
function releaseCards(releases) {
|
|
return (Array.isArray(releases) ? releases : [])
|
|
.filter(r => r && (r.tag_name || r.name))
|
|
.map((r) => {
|
|
const tag = String(r.tag_name || '').trim();
|
|
const name = String(r.name || '').trim();
|
|
const title = name && name !== tag ? `${tag} ${name}`.trim() : (tag || name);
|
|
const flags = [r.draft ? 'draft' : null, r.prerelease ? 'pre-release' : null].filter(Boolean);
|
|
return {
|
|
title: flags.length ? `${title} (${flags.join(', ')})` : title,
|
|
description: [String(r.body || '').trim(), r.html_url ? `Source: ${r.html_url}` : ''].filter(Boolean).join('\n\n'),
|
|
receivedAt: r.published_at || r.created_at || null,
|
|
assets: (Array.isArray(r.assets) ? r.assets : [])
|
|
.filter(a => a && typeof a.browser_download_url === 'string' && /^https:\/\//.test(a.browser_download_url))
|
|
.map(a => ({ name: String(a.name || 'asset'), url: a.browser_download_url, size: Number(a.size) || 0 })),
|
|
};
|
|
});
|
|
}
|
|
|
|
module.exports = { PARTS, DEFAULT_PARTS, RELEASES_LIST_TITLE, MAX_REPOS, ownerFrom, cleanOrgImport, selectRepos, releaseCards };
|