git-bot-feedback 0.7.1

A library designed for CI tools that posts comments on a Pull Request.
Documentation
# This script automates the release process for all of the packages in this repository.
# In order, this script does the following:
#
# 1. Bump version number in appropriate Cargo.toml manifest.
#
#    This step requires `cargo-edit` installed unless
#    releasing a binding package. In case of binding package, the version is
#    determined by the version of the img-gen crate, so no actual bumping is done.
#
# 2. Updates the appropriate CHANGELOG.md
#
#    Requires `git-cliff` (see https://git-cliff.org) to be installed
#    to regenerate the change logs from git history.
#
#    NOTE: `git cliff` uses GITHUB_TOKEN env var to access GitHub's REST API for
#    fetching certain data (like PR labels and commit author's username).
#
# 3. Pushes the changes from (steps 1 and 2) to remote
#
# 4. Creates a GitHub Release and uses the section from the CHANGELOG about the new tag
#    as a release description.
#
#    Requires `gh-cli` (see https://cli.github.com) to be installed to create the release
#    and push the tag.
#
#    NOTE: This step also tags the commit from step 3.
#    When a tag is pushed to the remote, the CI builds are triggered and
#    a package are published to crates.io
#
#    NOTE: In a CI run, the GITHUB_TOKEN env var to authenticate access.
#    Locally, you can use `gh login` to interactively authenticate the user account.
#
# The GITHUB_TOKEN permissions shall include:
# - read access to Pull Requests (for better CHANGELOG generation).
# - write (and inherently read) access to the repository "Contents"
#   for publishing a GitHub release and pushing metadata changes.

use ../common.nu run-cmd

const COMMON_EXCLUDES = [
    '.github/**/*'
    'docs/**/*'
    '.config/*'
    'README.md'
    '.gitattributes'
    '.gitignore'
    '.pre-commit-config.yaml'
    'bindings/python/README.md'
    'package.json'
    'codecov.yml'
    'Cargo.toml'
    'cspell.config.yml'
    '**/CHANGELOG.md'
]

const PkgPaths = {
    'git-bot-feedback': {
        include: ['src/**/*']
        exclude: [...$COMMON_EXCLUDES]
        path: '.'
    },
    'git-bot-feedback-py': {
        include: ['bindings/python/**/*', 'src/**/*.rs', 'Cargo.toml']
        exclude: [...$COMMON_EXCLUDES]
        path: 'bindings/python'
    },
}

export def get-changed-pkgs [] {
    let head_ref = $env | get --optional HEAD_REF | default 'HEAD'
    let base_ref = $env | get --optional BASE_REF | default 'HEAD~1'
    let changed_files = (
        (^git diff --name-only $base_ref $head_ref)
        | lines
        | str trim
        | where {not ($in | str starts-with ".")}
    )
    print "Changed files:"
    print $changed_files
    mut pkgs = []
    for row in ($PkgPaths | transpose) {
        let pkg = $row.column0
        let paths = $row.column1
        print $"Checking changes for ($pkg)..."
        let has_changed = if ($paths.include | is-empty) { true } else {
            $changed_files | any {|file| $paths.include | any {|p| ($file | path expand) in (glob $p)}}
        }
        print $"  Has changes: ($has_changed)"
        let has_excluded_change = if ($paths.exclude | is-empty) { false } else {
            $changed_files | any {|file| $paths.exclude | any {|p| ($file | path expand) in (glob $p)}}
        }
        print $"  Has excluded changes: ($has_excluded_change)"
        if $has_changed {
            print $" Package ($pkg) has relevant changes"
            $pkgs = $pkgs | append $pkg
        }
    }
    if ($pkgs | length) == 0 {
        print "No packages changed in the last commit."
    } else {
        print "Changed packages:"
        print $pkgs
    }
    $pkgs | to json --raw
}

# Is this executed in a CI run?
#
# Uses env var CI to determine the resulting boolean
export def is-in-ci [] {
    $env | get --optional CI | default 'false' | (($in == 'true') or ($in == true))
}

# Bump the version per the given component name (major, minor, patch)
#
# This function also updates known occurrences of the old version spec to
# the new version spec in various places (like README.md and action.yml).
export def bump-version [
    pkg: string, # The crate name to bump in respective Cargo.toml manifests
    component: string, # The version component to bump
] {
    mut args = ['-p', $pkg, '--bump', $component]
    if (not (is-in-ci)) {
        $args = $args | append ['--dry-run']
    }
    let result = cargo 'set-version' ...$args | complete
    if ($result.exit_code != 0) {
        error make {msg: $"cargo set-version failed: ($result.stderr)"}
    }
    let parsed = (
        $result.stderr
        | lines
        | where {|line| $line | str trim | str starts-with 'Upgrading'}
        | first
        | str trim
        | parse 'Upgrading {pkg} from {old} to {new}'
    )
    if ($parsed | is-empty) {
        error make {msg: $"Failed to parse version from: ($result.stderr)"}
    }
    let result = $parsed | first

    print $"bumped ($result | get 'old') to ($result | get 'new')"
    $result | get new
}

# Use `git-cliff` tp generate changes.
#
# If `--unreleased` is asserted, then the `git-cliff` output will be saved to .config/ReleaseNotes.md.
# Otherwise, the generated changes will span the entire git history and be saved to CHANGELOG.md.
export def gen-changes [
    pkg: string, # The crate name being bumped.
    --tag (-t): string = '', # The new version tag to use for unreleased changes.
    --unreleased (-u), # only generate changes from unreleased version.
] {
    let paths = $PkgPaths | get $pkg
    let path = $paths | get path | path expand
    let config_path = '.config' | path expand

    mut args = [
        '--config' $"($config_path | path join 'cliff.toml')"
    ]
    if (($tag | str length) > 0) {
        $args = $args | append ['--tag', $tag, '--tag-pattern', $"($pkg)/v*"]
    }
    let prompt = if $unreleased {
        let out_path = $config_path | path join 'ReleaseNotes.md'
        $args = $args | append [
            '--strip', 'header', '--unreleased', '--output', $out_path
        ]
        {out_path: ($out_path | path relative-to (pwd)), log_prefix: 'Generated'}
    } else {
        let out_path = $path | path expand | path join 'CHANGELOG.md'
        $args = $args | append [--output $out_path]
        {out_path: ($out_path | path relative-to (pwd)), log_prefix: 'Updated'}
    }
    if (($paths | get 'include' | length) > 0) {
        $args = $args | append ['--include-path', ...($paths | get 'include')]
    }
    if (($paths | get 'exclude' | length) > 0) {
        $args = $args | append ['--exclude-path', ...($paths | get 'exclude')]
    }
    let args = $args # make args immutable (to use in `with-env` block below)
    with-env {GIT_CLIFF_TAG: $tag} {
        run-cmd 'git-cliff' ...$args
    }
    print ($prompt | format pattern '{log_prefix} {out_path}')
}

# Is the the default branch currently checked out?
#
# Only accurate if the default branch is named "main".
export def is-on-main [] {
    let branch = (
        ^git branch
        | lines
        | where {$in | str starts-with '*'}
        | first
        | str trim --left --char '*'
        | str trim
    ) == 'main'
    $branch
}

# The main function of this script.
#
# The `pkg` and `component` parameters are required CLI options:
#     nu .github/workflows/bump-n-release.nu img-gen patch
#
# The acceptable `pkg` value are defined in the Cargo.toml manifests' `[package.name]` field.
#
# The acceptable `component` values are what `cargo set-version` accepts:
#
# - major
# - minor
# - patch
# - rc
def main [
    pkg: string, # The crate name to bump in respective Cargo.toml manifests
    component: string, # The version component to bump
] {
    let ver = bump-version $pkg $component
    let tag = $"($pkg)/v($ver)"
    gen-changes $pkg --tag $tag
    gen-changes $pkg --tag $tag --unreleased
    let is_main = is-on-main
    if not $is_main {
        print $"(ansi yellow)\nNot checked out on default branch!(ansi reset)"
    }
    if (is-in-ci) and $is_main {
        print 'Pushing metadata changes'
        run-cmd git config --global user.name $"($env.GITHUB_ACTOR)"
        run-cmd git config --global user.email $"($env.GITHUB_ACTOR_ID)+($env.GITHUB_ACTOR)@users.noreply.github.com"
        run-cmd git add --all
        run-cmd git commit -m $"build: bump version to ($tag)"
        run-cmd git push
        print $"Deploying ($tag)"
        run-cmd gh release create $tag --notes-file .config/ReleaseNotes.md --title $"($pkg) v($ver)"
    } else if $is_main {
        print $"(ansi yellow)Not deploying from local clone.(ansi reset)"
    }
}