Skip to content

Instantly share code, notes, and snippets.

@flavio
Last active July 23, 2025 16:36
Show Gist options
  • Select an option

  • Save flavio/f13ab7ebd9b421a4602a7569a9100a50 to your computer and use it in GitHub Desktop.

Select an option

Save flavio/f13ab7ebd9b421a4602a7569a9100a50 to your computer and use it in GitHub Desktop.

Revisions

  1. flavio revised this gist Jul 23, 2025. No changes.
  2. flavio revised this gist Jul 23, 2025. 1 changed file with 50 additions and 0 deletions.
    50 changes: 50 additions & 0 deletions org-backup.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,50 @@
    require 'octokit'

    # Usage:
    # ruby org-backup.rb save org_name
    # ruby org-backup.rb restore org_name
    # (expects token in .github_token file in current directory or HOME)

    def read_token
    paths = [
    File.join(Dir.pwd, ".github_token"),
    File.join(Dir.home, ".github_token")
    ]
    paths.each do |path|
    return File.read(path).strip if File.exist?(path)
    end
    puts "GitHub token not found. Please create a .github_token file in the current directory or your home directory."
    exit 1
    end

    if ARGV.length != 2 || !%w[save restore].include?(ARGV[0])
    puts "Usage:"
    puts " ruby org-backup.rb save org_name"
    puts " ruby org-backup.rb restore org_name"
    exit 1
    end

    mode, org = ARGV
    token = read_token

    client = Octokit::Client.new(access_token: token)
    client.auto_paginate = true

    repos = client.org_repos(org, type: 'all').map(&:full_name)
    total = repos.size

    repos.each_with_index do |repo, idx|
    file = "#{repo.gsub('/', '__')}.json"
    progress = "#{idx + 1}/#{total}"
    if mode == "save"
    puts "[#{progress}] Saving labels for #{repo}..."
    system("ruby repo-backup.rb save #{repo} #{file}")
    else
    if File.exist?(file)
    puts "[#{progress}] Restoring labels for #{repo}..."
    system("ruby repo-backup.rb restore #{repo} #{file}")
    else
    puts "[#{progress}] Backup file #{file} not found for #{repo}, skipping."
    end
    end
    end
  3. flavio created this gist Jul 23, 2025.
    93 changes: 93 additions & 0 deletions repo-backup.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,93 @@
    require 'octokit'
    require 'json'

    # Usage:
    # ruby repo-backup.rb save owner/repo output.json
    # ruby repo-backup.rb restore owner/repo input.json
    # (expects token in .github_token file in the current directory or HOME)

    def read_token
    paths = [
    File.join(Dir.pwd, ".github_token"),
    File.join(Dir.home, ".github_token")
    ]
    paths.each do |path|
    return File.read(path).strip if File.exist?(path)
    end
    puts "GitHub token not found. Please create a .github_token file in the current directory or your home directory."
    exit 1
    end

    if ARGV.length != 3 || !%w[save restore].include?(ARGV[0])
    puts "Usage:"
    puts " ruby repo-backup.rb save owner/repo output.json"
    puts " ruby repo-backup.rb restore owner/repo input.json"
    puts " (expects token in .github_token file in current directory or HOME)"
    exit 1
    end

    mode, repo, file = ARGV
    token = read_token

    client = Octokit::Client.new(access_token: token)
    client.auto_paginate = true

    def fetch_labels_for_items(client, repo, type)
    items = []
    page = 1
    loop do
    batch = if type == :issue
    client.issues(repo, state: 'all', per_page: 100, page: page)
    else
    client.pull_requests(repo, state: 'all', per_page: 100, page: page)
    end
    break if batch.empty?
    items.concat(batch)
    page += 1
    end

    items.map do |item|
    {
    id: item.id,
    number: item.number,
    title: item.title,
    labels: item.labels.map { |l| l.name }
    }
    end
    end

    if mode == "save"
    puts "Fetching issues..."
    issues = fetch_labels_for_items(client, repo, :issue)
    puts "Fetching pull requests..."
    prs = fetch_labels_for_items(client, repo, :pr)

    result = {
    issues: issues,
    pull_requests: prs
    }

    File.write(file, JSON.pretty_generate(result))
    puts "Saved associations to #{file}"
    elsif mode == "restore"
    data = JSON.parse(File.read(file))
    puts "Restoring issue labels..."
    data["issues"].each do |issue|
    begin
    client.update_issue(repo, issue["number"], labels: issue["labels"])
    puts "Restored labels for issue ##{issue["number"]}"
    rescue => e
    puts "Failed to restore labels for issue ##{issue["number"]}: #{e}"
    end
    end

    puts "Restoring pull request labels..."
    data["pull_requests"].each do |pr|
    begin
    client.update_issue(repo, pr["number"], labels: pr["labels"])
    puts "Restored labels for pull request ##{pr["number"]}"
    rescue => e
    puts "Failed to restore labels for pull request ##{pr["number"]}: #{e}"
    end
    end
    end