diff --git a/.eslintignore b/.eslintignore index 11d9cf3bd..b2a8ba86b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -928,6 +928,7 @@ packages/tools/git-changelog.test.js packages/tools/git-changelog.js packages/tools/licenseChecker.js packages/tools/packageJsonLint.js +packages/tools/postPreReleasesToForum.js packages/tools/release-android.js packages/tools/release-cli.js packages/tools/release-electron.js diff --git a/.gitignore b/.gitignore index dbaaeb22b..9bf282289 100644 --- a/.gitignore +++ b/.gitignore @@ -914,6 +914,7 @@ packages/tools/git-changelog.test.js packages/tools/git-changelog.js packages/tools/licenseChecker.js packages/tools/packageJsonLint.js +packages/tools/postPreReleasesToForum.js packages/tools/release-android.js packages/tools/release-cli.js packages/tools/release-electron.js diff --git a/package.json b/package.json index 3251cd48b..b4f04393d 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "buildPluginDoc": "cd packages/generate-plugin-doc && yarn run buildPluginDoc_", "updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc", "updateNews": "node ./packages/tools/website/updateNews", + "postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum", "buildSettingJsonSchema": "yarn workspace joplin start settingschema ../../../joplin-website/docs/schema/settings.json", "buildTranslations": "node packages/tools/build-translation.js", "buildWebsiteTranslations": "node packages/tools/website/buildTranslations.js", diff --git a/packages/tools/postPreReleasesToForum.ts b/packages/tools/postPreReleasesToForum.ts new file mode 100644 index 000000000..83287537d --- /dev/null +++ b/packages/tools/postPreReleasesToForum.ts @@ -0,0 +1,99 @@ +import { pathExists } from 'fs-extra'; +import { readFile, writeFile } from 'fs/promises'; +import { gitHubLatestReleases } from './tool-utils'; +import { config, createPost, createTopic, getForumTopPostByExternalId, getTopicByExternalId, updatePost } from './utils/discourse'; +import * as compareVersions from 'compare-versions'; +import dayjs = require('dayjs'); + +interface State { + processedReleases: Record; +} + +const stateFilePath = `${__dirname}/postPreReleasesToForum.json`; +const betaCategoryId = 10; + +const loadState = async (): Promise => { + if (await pathExists(stateFilePath)) { + const content = await readFile(stateFilePath, 'utf-8'); + return JSON.parse(content) as State; + } + return { + processedReleases: {}, + }; +}; + +const saveState = async (state: State) => { + await writeFile(stateFilePath, JSON.stringify(state), 'utf-8'); +}; + +const getMinorVersion = (fullVersion: string) => { + const s = fullVersion.substring(1).split('.'); + return `${s[0]}.${s[1]}`; +}; + +const main = async () => { + const argv = require('yargs').argv; + config.key = argv._[0]; + config.username = argv._[1]; + + const state = await loadState(); + const releases = await gitHubLatestReleases(1, 50); + + releases.sort((a, b) => { + return compareVersions(a.tag_name, b.tag_name) <= 0 ? -1 : +1; + }); + + const startFromVersion = '2.13'; + + for (const release of releases) { + const minorVersion = getMinorVersion(release.tag_name); + + if (compareVersions(startFromVersion, minorVersion) > 0) continue; + + if (!state.processedReleases[release.tag_name]) { + console.info(`Processing release ${release.tag_name}`); + + const externalId = `prerelease-${minorVersion.replace(/\./g, '-')}`; + + const postBody = `## [${release.tag_name}](${release.html_url})\n\n${release.body}`; + + let topic = await getTopicByExternalId(externalId); + + const topicTitle = `Pre-release v${minorVersion} is now available (Updated ${dayjs(new Date()).format('DD/MM/YYYY')})`; + + if (!topic) { + console.info('No topic exists - creating one...'); + + topic = await createTopic({ + title: topicTitle, + raw: `Download the latest pre-release from here: \n\n* * *\n\n${postBody}`, + category: betaCategoryId, + external_id: externalId, + }); + } else { + console.info('A topic exists - appending the new pre-release to it...'); + + const topPost = await getForumTopPostByExternalId(externalId); + + await updatePost(topPost.id, { + title: topicTitle, + raw: `${topPost.raw}\n\n${postBody}`, + edit_reason: 'Auto-updated by script', + }); + + await createPost(topic.id, { + raw: postBody, + }); + } + + state.processedReleases[release.tag_name] = true; + + await saveState(state); + } + } +}; + +main().catch((error) => { + console.error('Fatal error', error); + process.exit(1); +}); diff --git a/packages/tools/release-website.sh b/packages/tools/release-website.sh index 28f941a49..dc45e192f 100755 --- a/packages/tools/release-website.sh +++ b/packages/tools/release-website.sh @@ -41,7 +41,10 @@ yarn install git reset --hard JOPLIN_GITHUB_OAUTH_TOKEN=$JOPLIN_GITHUB_OAUTH_TOKEN yarn run updateMarkdownDoc + +# Automatically update certain forum posts yarn run updateNews $DISCOURSE_API_KEY $DISCOURSE_USERNAME +yarn run postPreReleasesToForum $DISCOURSE_API_KEY $DISCOURSE_USERNAME # We commit and push the change. It will be a noop if nothing was actually # changed diff --git a/packages/tools/tool-utils.ts b/packages/tools/tool-utils.ts index 21dd0a234..bfd94906c 100644 --- a/packages/tools/tool-utils.ts +++ b/packages/tools/tool-utils.ts @@ -18,6 +18,7 @@ export interface GitHubRelease { html_url: string; prerelease: boolean; draft: boolean; + body: string; } async function insertChangelog(tag: string, changelogPath: string, changelog: string, isPrerelease: boolean, repoTagUrl = '') { @@ -375,6 +376,22 @@ export async function gitHubLatestRelease_KeepInCaseMicrosoftBreaksTheApiAgain(r } } +export const gitHubLatestReleases = async (page: number, perPage: number) => { + const response = await fetch(`https://api.github.com/repos/laurent22/joplin/releases?page=${page}&per_page=${perPage}`, { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Joplin Forum Updater', + }, + }); + + if (!response.ok) throw new Error(`Cannot fetch releases: ${response.statusText}`); + + const releases: GitHubRelease[] = await response.json(); + if (!releases.length) throw new Error('Cannot find latest release'); + + return releases; +}; + export async function githubRelease(project: string, tagName: string, options: any = null): Promise { options = { isDraft: false, isPreRelease: false, ...options }; diff --git a/packages/tools/utils/discourse.ts b/packages/tools/utils/discourse.ts index 42b0617db..7d1f7f8c0 100644 --- a/packages/tools/utils/discourse.ts +++ b/packages/tools/utils/discourse.ts @@ -21,6 +21,11 @@ interface ForumTopPost { title: string; } +interface ForumTopic { + id: number; + topic_id: string; +} + export const config: ApiConfig = { baseUrl: 'https://discourse.joplinapp.org', key: '', @@ -61,6 +66,7 @@ export const execApi = async (method: HttpMethod, path: string, body: Record => { + try { + const existingForumTopic = await execApi(HttpMethod.GET, `t/external_id/${externalId}.json`); + return existingForumTopic; + } catch (error) { + if (error.status === 404) return null; + if (error.apiObject && error.apiObject.error_type === 'not_found') return null; + throw error; + } +}; + +export const createTopic = async (topic: any): Promise => { + return execApi(HttpMethod.POST, 'posts', topic); +}; + +export const createPost = async (topicId: number, post: any): Promise => { + return execApi(HttpMethod.POST, 'posts', { + topic_id: topicId, + ...post, + }); +}; + +export const updatePost = async (postId: number, content: any): Promise => { + await execApi(HttpMethod.PUT, `posts/${postId}.json`, content); +}; diff --git a/packages/tools/website/updateNews.ts b/packages/tools/website/updateNews.ts index 5e005d59e..7fb2c7308 100644 --- a/packages/tools/website/updateNews.ts +++ b/packages/tools/website/updateNews.ts @@ -8,7 +8,7 @@ import { rootDir } from '../tool-utils'; import { compileWithFrontMatter, MarkdownAndFrontMatter, stripOffFrontMatter } from './utils/frontMatter'; import { markdownToHtml } from './utils/render'; import { getNewsDate } from './utils/news'; -import { config, execApi, getForumTopPostByExternalId, HttpMethod } from '../utils/discourse'; +import { config, createTopic, getForumTopPostByExternalId, updatePost } from '../utils/discourse'; const RSS = require('rss'); interface Post { @@ -144,7 +144,7 @@ const main = async () => { } else { console.info('Post already exists and has changed: updating it...'); - await execApi(HttpMethod.PUT, `posts/${existingForumPost.id}.json`, { + await updatePost(existingForumPost.id, { title: content.title, raw: content.body, edit_reason: 'Auto-updated by script', @@ -153,14 +153,14 @@ const main = async () => { } else { console.info('Post does not exists: creating it...'); - const response = await execApi(HttpMethod.POST, 'posts', { + const topic = await createTopic({ title: content.title, raw: content.body, category: config.newsCategoryId, external_id: post.id, }); - const postUrl = `https://discourse.joplinapp.org/t/${response.topic_id}`; + const postUrl = `https://discourse.joplinapp.org/t/${topic.topic_id}`; content.parsed.forum_url = postUrl; const compiled = compileWithFrontMatter(content.parsed);