diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml index f37031f61e9..1875e6154be 100644 --- a/.github/workflows/wiki.yml +++ b/.github/workflows/wiki.yml @@ -1,20 +1,41 @@ name: Deploy to Wiki on: + pull_request: + paths: + - 'wiki/**' push: branches: - develop paths: - 'wiki/**' - # Triggers this workflow when the wiki is changed + # Triggers this workflow when the wiki is changed. # (see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum). gollum: jobs: + table_of_contents_check: + # To verify that the wiki's table of contents matches the headers accurately. + name: Check Wiki Table of Contents + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Set up Bazel + uses: abhinavsingh/setup-bazel@v3 + with: + version: 6.5.0 + + - name: Check Wiki Table of Contents + id: checkWikiToc + run: | + bazel run //scripts:wiki_table_of_contents_check -- ${GITHUB_WORKSPACE} + wiki-deploy: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-20.04] + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }} steps: - uses: actions/checkout@v3 with: diff --git a/scripts/BUILD.bazel b/scripts/BUILD.bazel index 6ef9a5d6739..9577dfe834b 100644 --- a/scripts/BUILD.bazel +++ b/scripts/BUILD.bazel @@ -237,6 +237,15 @@ kt_jvm_binary( ], ) +kt_jvm_binary( + name = "wiki_table_of_contents_check", + testonly = True, + main_class = "org.oppia.android.scripts.wiki.WikiTableOfContentsCheckKt", + runtime_deps = [ + "//scripts/src/java/org/oppia/android/scripts/wiki:wiki_table_of_contents_check_lib", + ], +) + kt_jvm_binary( name = "run_coverage", testonly = True, diff --git a/scripts/src/java/org/oppia/android/scripts/wiki/BUILD.bazel b/scripts/src/java/org/oppia/android/scripts/wiki/BUILD.bazel new file mode 100644 index 00000000000..6898d1b8c21 --- /dev/null +++ b/scripts/src/java/org/oppia/android/scripts/wiki/BUILD.bazel @@ -0,0 +1,18 @@ +""" +Libraries corresponding to scripting tools that help with continuous integration workflows. +""" + +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_jvm_library") + +kt_jvm_library( + name = "wiki_table_of_contents_check_lib", + testonly = True, + srcs = [ + "WikiTableOfContentsCheck.kt", + ], + visibility = ["//scripts:oppia_script_binary_visibility"], + deps = [ + "//scripts/src/java/org/oppia/android/scripts/common:bazel_client", + "//scripts/src/java/org/oppia/android/scripts/common:git_client", + ], +) diff --git a/scripts/src/java/org/oppia/android/scripts/wiki/WikiTableOfContentsCheck.kt b/scripts/src/java/org/oppia/android/scripts/wiki/WikiTableOfContentsCheck.kt new file mode 100644 index 00000000000..c634792e7a9 --- /dev/null +++ b/scripts/src/java/org/oppia/android/scripts/wiki/WikiTableOfContentsCheck.kt @@ -0,0 +1,98 @@ +package org.oppia.android.scripts.wiki + +import java.io.File + +/** + * Script for ensuring that the table of contents in each wiki page matches with its respective headers. + * + * Usage: + * bazel run //scripts:wiki_table_of_contents_check -- + * + * Arguments: + * - path_to_default_working_directory: The default working directory on the runner for steps, and the default location of repository. + * + * Example: + * bazel run //scripts:wiki_table_of_contents_check -- $(pwd) + */ +fun main(vararg args: String) { + // Path to the repo's wiki. + val wikiDirPath = "${args[0]}/wiki/" + val wikiDir = File(wikiDirPath) + + // Check if the wiki directory exists. + if (wikiDir.exists() && wikiDir.isDirectory) { + processWikiDirectory(wikiDir) + println("WIKI TABLE OF CONTENTS CHECK PASSED") + } else { + println("No contents found in the Wiki directory.") + } +} + +/** + * Checks every file in the wiki repo. + * + * @param wikiDir the default working directory + */ +fun processWikiDirectory(wikiDir: File) { + wikiDir.listFiles()?.forEach { file -> + checkTableOfContents(file) + } +} + +/** + * Checks the contents of a single wiki file to ensure the accuracy of the Table of Contents. + * + * @param file the wiki file to process. + */ +fun checkTableOfContents(file: File) { + val fileContents = file.readLines() + val tocStartIdx = fileContents.indexOfFirst { + it.contains(Regex("""##\s+Table\s+of\s+Contents""", RegexOption.IGNORE_CASE)) + } + if (tocStartIdx == -1) { + return + } + + // Skipping the blank line after the ## Table of Contents + val eOfIdx = fileContents.subList(tocStartIdx + 2, fileContents.size).indexOfFirst { + it.isBlank() + } + if (eOfIdx == -1) error("Table of Contents didn't end with a blank line.") + + val tocSpecificLines = fileContents.subList(tocStartIdx, tocStartIdx + eOfIdx + 1) + + for (line in tocSpecificLines) { + if (line.trimStart().startsWith("- [") && !line.contains("https://")) { + validateTableOfContents(file, line) + } + } +} + +/** + * Validates the accuracy of a Table of Contents entry in a wiki file. + * + * @param file the wiki file being validated. + * @param line the line containing the Table of Contents entry. + */ +fun validateTableOfContents(file: File, line: String) { + val titleRegex = "\\[(.*?)\\]".toRegex() + val title = titleRegex.find(line)?.groupValues?.get(1)?.replace('-', ' ') + ?.replace(Regex("[?&./:’'*!,(){}\\[\\]+]"), "") + ?.trim() + + val linkRegex = "\\(#(.*?)\\)".toRegex() + val link = linkRegex.find(line)?.groupValues?.get(1)?.removePrefix("#")?.replace('-', ' ') + ?.replace(Regex("[?&./:’'*!,(){}\\[\\]+]"), "") + ?.trim() + + // Checks if the table of content title matches with the header link text. + val matches = title.equals(link, ignoreCase = true) + if (!matches) { + error( + "\nWIKI TABLE OF CONTENTS CHECK FAILED" + + "\nMismatch of Table of Content with headers in the File: ${file.name}. " + + "\nThe Title: '${titleRegex.find(line)?.groupValues?.get(1)}' " + + "doesn't match with its corresponding Link: '${linkRegex.find(line)?.groupValues?.get(1)}'." + ) + } +} diff --git a/scripts/src/javatests/org/oppia/android/scripts/wiki/BUILD.bazel b/scripts/src/javatests/org/oppia/android/scripts/wiki/BUILD.bazel new file mode 100644 index 00000000000..953b3f7d8d9 --- /dev/null +++ b/scripts/src/javatests/org/oppia/android/scripts/wiki/BUILD.bazel @@ -0,0 +1,16 @@ +""" +Tests corresponding to wiki-related checks. +""" + +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") + +kt_jvm_test( + name = "WikiTableOfContentsCheckTest", + srcs = ["WikiTableOfContentsCheckTest.kt"], + deps = [ + "//scripts/src/java/org/oppia/android/scripts/wiki:wiki_table_of_contents_check_lib", + "//testing:assertion_helpers", + "//third_party:com_google_truth_truth", + "//third_party:org_jetbrains_kotlin_kotlin-test-junit", + ], +) diff --git a/scripts/src/javatests/org/oppia/android/scripts/wiki/WikiTableOfContentsCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/wiki/WikiTableOfContentsCheckTest.kt new file mode 100644 index 00000000000..8b91a453107 --- /dev/null +++ b/scripts/src/javatests/org/oppia/android/scripts/wiki/WikiTableOfContentsCheckTest.kt @@ -0,0 +1,168 @@ +package org.oppia.android.scripts.wiki + +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.oppia.android.testing.assertThrows +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +/** Tests for [WikiTableOfContentsCheck]. */ +class WikiTableOfContentsCheckTest { + private val outContent: ByteArrayOutputStream = ByteArrayOutputStream() + private val originalOut: PrintStream = System.out + private val WIKI_TOC_CHECK_PASSED_OUTPUT_INDICATOR = "WIKI TABLE OF CONTENTS CHECK PASSED" + private val WIKI_TOC_CHECK_FAILED_OUTPUT_INDICATOR = "WIKI TABLE OF CONTENTS CHECK FAILED" + + @field:[Rule JvmField] val tempFolder = TemporaryFolder() + + @Before + fun setUp() { + System.setOut(PrintStream(outContent)) + } + + @After + fun tearDown() { + System.setOut(originalOut) + } + + @Test + fun testWikiTOCCheck_noWikiDirExists_printsNoContentFound() { + runScript() + assertThat(outContent.toString().trim()).isEqualTo("No contents found in the Wiki directory.") + } + + @Test + fun testWikiTOCCheck_noWikiDirectory_printsNoContentFound() { + tempFolder.newFile("wiki") + runScript() + assertThat(outContent.toString().trim()).isEqualTo("No contents found in the Wiki directory.") + } + + @Test + fun testWikiTOCCheck_validWikiTOC_checkPass() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + ## Table of Contents + + - [Introduction](#introduction) + - [Usage](#usage) + + ## Introduction + Content + + ## Usage + Content + """.trimIndent() + ) + + runScript() + + assertThat(outContent.toString().trim()).contains(WIKI_TOC_CHECK_PASSED_OUTPUT_INDICATOR) + } + + @Test + fun testWikiTOCCheck_missingWikiTOC_returnsNoTOCFound() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + - [Introduction](#introduction) + - [Usage](#usage) + + ## Introduction + Content + + ## Usage + Content + """.trimIndent() + ) + + runScript() + + assertThat(outContent.toString().trim()).contains(WIKI_TOC_CHECK_PASSED_OUTPUT_INDICATOR) + } + + @Test + fun testWikiTOCCheck_mismatchWikiTOC_checkFail() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + ## Table of Contents + + - [Introduction](#introductions) + - [Usage](#usage) + + ## Introduction + Content + + ## Usage + Content + """.trimIndent() + ) + + val exception = assertThrows() { + runScript() + } + + assertThat(exception).hasMessageThat().contains(WIKI_TOC_CHECK_FAILED_OUTPUT_INDICATOR) + } + + @Test + fun testWikiTOCCheck_validWikiTOCWithSeparator_checkPass() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + ## Table of Contents + + - [Introduction To Wiki](#introduction-to-wiki) + - [Usage Wiki-Content](#usage-wiki-content) + + ## Introduction + Content + + ## Usage + Content + """.trimIndent() + ) + + runScript() + + assertThat(outContent.toString().trim()).contains(WIKI_TOC_CHECK_PASSED_OUTPUT_INDICATOR) + } + + @Test + fun testWikiTOCCheck_validWikiTOCWithSpecialCharacter_checkPass() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + ## Table of Contents + + - [Introduction](#introduction?) + - [Usage?](#usage) + + ## Introduction + Content + + ## Usage + Content + """.trimIndent() + ) + + runScript() + + assertThat(outContent.toString().trim()).contains(WIKI_TOC_CHECK_PASSED_OUTPUT_INDICATOR) + } + + private fun runScript() { + main(tempFolder.root.absolutePath) + } +}