Skip to content

Instantly share code, notes, and snippets.

@mpetuska
Last active February 24, 2022 14:57
Show Gist options
  • Save mpetuska/7d2cab8500ba18d961e61e70d4c0a612 to your computer and use it in GitHub Desktop.
Save mpetuska/7d2cab8500ba18d961e61e70d4c0a612 to your computer and use it in GitHub Desktop.
Script to sync milestone issues to the project

This script allows managing issue synchronisation between a github milestone and a project. Supports both, importing and clearing of milestone issues.

Usage

  1. Install kotlin (SDKMAN recommended): sdk install kotlin

  2. Install script

  3. wget https://gist.githubusercontent.com/mpetuska/7d2cab8500ba18d961e61e70d4c0a612/raw/ghImportMilestoneToProject.main.kts

    OR

    curl https://gist.githubusercontent.com/mpetuska/7d2cab8500ba18d961e61e70d4c0a612/raw/ghImportMilestoneToProject.main.kts -o ghImportMilestoneToProject.main.kts

  4. chmod +x ghImportMilestoneToProject.main.kts

  5. Run the script: ./ghImportMilestoneToProject.main.kts --help

#!/usr/bin/env kotlin
@file:DependsOn("org.kohsuke:github-api:1.301")
@file:DependsOn("com.github.ajalt.clikt:clikt-jvm:3.4.0")
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.options.defaultLazy
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.multiple
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.prompt
import org.kohsuke.github.GHIssue
import org.kohsuke.github.GHIssueState
import org.kohsuke.github.GHMilestoneState
import org.kohsuke.github.GHProject
import org.kohsuke.github.GHProjectCard
import org.kohsuke.github.GHProjectColumn
import org.kohsuke.github.GitHub
CLI().subcommands(Import(), Clear()).main(args)
class CLI : CliktCommand(
name = "ghImportMilestoneToProject.main.kts",
epilog = "GitHub authentication is handled by https://github-api.kohsuke.org",
printHelpOnEmptyArgs = true
) {
override fun run() {}
}
abstract class BaseCommand : CliktCommand() {
val dry: Boolean by option(help = "Only report changes and skip writing them").flag()
val repository: String by option(help = "The name of the repository").prompt("Repository")
val milestone: String by option(help = "The name of the milestone to source the issues").prompt("Milestone")
val project: String by option(help = "The name of the project to import the issues").defaultLazy(defaultForHelp = "milestone") { milestone }
}
class Import : BaseCommand() {
val open: String? by option(help = "The name of the column to import open issues")
val closed: String? by option(help = "The name of the column to import closed issues")
override fun run() {
if (open == null && closed == null) error("No columns specified")
with(GitHub.connect()) {
val repo = getRepository(repository)
val mil = repo.listMilestones(GHIssueState.ALL).firstOrNull { it.title == milestone }
?: error("Milestone '$milestone' not found")
if (mil.state == GHMilestoneState.CLOSED) error("Milestone '${mil.title}' is closed")
val proj: GHProject = repo.listProjects(GHProject.ProjectStateFilter.ALL).firstOrNull { it.name == project }
?: error("Project '$project' not found")
if (proj.state == GHProject.ProjectState.CLOSED) {
error("Project '${proj.name}' is closed")
} else {
val columns: Iterable<GHProjectColumn> = proj.listColumns()
val projectIssues by lazy {
columns.flatMap(GHProjectColumn::listCards).map(GHProjectCard::getContent)
}
val projectIssueIds by lazy {
projectIssues.map(GHIssue::getId)
}
if (open != null) {
val openColumn = columns.firstOrNull { it.name == open } ?: error("Open column '$open' not found")
openColumn.addIssues(projectIssueIds, repo.getIssues(GHIssueState.OPEN, mil))
}
if (closed != null) {
val closedColumn = columns.firstOrNull { it.name == closed } ?: error("Closed column '$closed' not found")
closedColumn.addIssues(projectIssueIds, repo.getIssues(GHIssueState.CLOSED, mil))
}
}
}
}
private fun GHProjectColumn.addIssues(existing: Iterable<Long>, toAdd: Iterable<GHIssue>) {
toAdd.filter { it.id !in existing }.forEach {
if (!dry) {
val card: GHProjectCard = createCard(it)
println("Card created: column=${this.name}, card=${card.htmlUrl}")
} else {
println("Card to create: column=${this.name}, issue=${it.htmlUrl}")
}
}
}
}
class Clear : BaseCommand() {
val column: List<String> by option(help = "The names of the columns to clear").multiple()
override fun run() {
with(GitHub.connect()) {
val repo = getRepository(repository)
val mil = repo.listMilestones(GHIssueState.ALL).firstOrNull { it.title == milestone }
?: error("Milestone '$milestone' not found")
if (mil.state == GHMilestoneState.CLOSED) error("Milestone '${mil.title}' is closed")
val proj: GHProject = repo.listProjects(GHProject.ProjectStateFilter.ALL).firstOrNull { it.name == project }
?: error("Project '$project' not found")
if (proj.state == GHProject.ProjectState.CLOSED) {
error("Project '${proj.name}' is closed")
} else {
val columns: Iterable<GHProjectColumn> = proj.listColumns()
val targetColumns = if (column.isEmpty()) {
columns
} else {
column.map { c -> columns.firstOrNull { it.name == c } ?: error("Column '$c' not found") }
}
targetColumns.forEach { c ->
val cards = c.listCards().filter {
it.content?.milestone?.title == milestone
}
cards.forEach {
if (!dry) {
it.delete()
println("Card removed: column=${c.name}, issue=${it.content.htmlUrl}")
} else {
println("Card to remove: column=${c.name}, issue=${it.content.htmlUrl}")
}
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment