Skip to content

Commit

Permalink
Make the bobg/modver repo suitable for use in GitHub Actions (bobg#15)
Browse files Browse the repository at this point in the history
* Checkpoint.

* Checkpoint. Refactor some stuff to internal and create cmd/modver-action.

* go mod tidy; move a test

* More test coverage.

* INPUT_GITHUB_TOKEN

* Add GOROOT/bin to PATH in modver-action.

* Not PATH but GOROOT needs to be set.

* Show failure from go env

* More test coverage.

* Is this a thing?

* Will this work?

* Iterate.

* Iterate.

* Iterate.

* Iterate.

* Iterate.

* Iterate.

* Iterate.

* Iterate.

* Update Readme with info about using Modver in GitHub Actions.
  • Loading branch information
bobg authored Apr 1, 2023
1 parent d6904db commit 9f38d1b
Show file tree
Hide file tree
Showing 16 changed files with 413 additions and 83 deletions.
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM golang:latest

ADD . /app

WORKDIR /app

RUN go build ./cmd/modver-action

ENTRYPOINT ["/app/modver-action"]
72 changes: 70 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@
[![Coverage Status](https://coveralls.io/repos/github/bobg/modver/badge.svg?branch=master)](https://coveralls.io/github/bobg/modver?branch=master)

This is modver,
a Go package and command that helps you obey [semantic versioning rules](https://semver.org/) in your Go module.
a tool that helps you obey [semantic versioning rules](https://semver.org/) in your Go module.

It can read and compare two different versions of the same module,
from two different directories,
or two different Git commits.
or two different Git commits,
or the base and head of a Git pull request.
It then reports whether the changes require an increase in the major-version number,
the minor-version number,
or the patchlevel.

## Installation and usage

Modver can be used from the command line,
or in your Go program,
or with [GitHub Actions](https://github.com/features/actions).

### Command-line interface

Install the `modver` command like this:

```sh
Expand All @@ -37,7 +44,68 @@ The arguments `HEAD~1` and `HEAD` specify two Git revisions to compare;
in this case, the latest two commits on the current branch.
These could also be tags or commit hashes.

### GitHub Action

You can arrange for Modver to inspect the changes on your pull-request branch
as part of a GitHub Actions-based continuous-integration step.
It will add a comment to the pull request with its findings,
and will update the comment as new commits are pushed to the branch.

To do this, you’ll need a directory in your GitHub repository named `.github/workflows`,
and a Yaml file containing (at least) the following:

```yaml
name: Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.19

- name: Modver
if: ${{ github.event_name == 'pull_request' }}
uses: bobg/[email protected]
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
pull_request_url: https://github.com/${{ github.repository }}/pull/${{ github.event.number }}
```
This can be combined with other steps that run unit tests, etc.
You can change `Tests` to whatever name you like,
and should change `main` to the name of your repository’s default branch.
If your pull request is on a GitHub server other than `github.com`,
change the hostname in the `pull_request_url` parameter to match.

Note the `fetch-depth: 0` parameter for the `Checkout` step.
This causes GitHub Actions to create a clone of your repo with its full history,
as opposed to the default,
which is a shallow clone.
Modver requires enough history to be present in the clone
for it to access the “base” and “head” revisions of your pull-request branch.

For more information about configuring GitHub Actions,
see [the GitHub Actions documentation](https://docs.github.com/actions).

### Go library

Modver also has a simple API for use from within Go programs.
Add it to your project with `go get github.com/bobg/modver/v2@latest`.
See [the Go doc page](https://pkg.go.dev/github.com/bobg/modver/v2) for information about how to use it.

## Semantic versioning

Expand Down
13 changes: 13 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Modver
description: Analyze pull requests for changes in Go code that require updating a module's version number.
author: Bob Glickstein
inputs:
github_token:
description: 'The GitHub token to use for authentication.'
required: true
pull_request_url:
description: 'The full github.com URL of the pull request.'
required: true
runs:
using: 'docker'
image: 'Dockerfile'
34 changes: 34 additions & 0 deletions cmd/modver-action/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package main

import (
"context"
"log"
"os"

"github.com/bobg/modver/v2"
"github.com/bobg/modver/v2/internal"
)

func main() {
os.Setenv("GOROOT", "/usr/local/go") // Work around some Docker weirdness.

prURL := os.Getenv("INPUT_PULL_REQUEST_URL")
host, owner, reponame, prnum, err := internal.ParsePR(prURL)
if err != nil {
log.Fatal(err)
}
token := os.Getenv("INPUT_GITHUB_TOKEN")
if token == "" {
log.Fatal("No GitHub token in the environment variable INPUT_GITHUB_TOKEN")
}
ctx := context.Background()
gh, err := internal.NewClient(ctx, host, token)
if err != nil {
log.Fatalf("Creating GitHub client: %s", err)
}
result, err := internal.PR(ctx, gh, owner, reponame, prnum)
if err != nil {
log.Fatalf("Running comparison: %s", err)
}
modver.Pretty(os.Stdout, result)
}
25 changes: 20 additions & 5 deletions cmd/modver/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,34 @@ import (
"github.com/pkg/errors"

"github.com/bobg/modver/v2"
"github.com/bobg/modver/v2/internal"
)

func doCompare(ctx context.Context, opts options) (modver.Result, error) {
return doCompareHelper(ctx, opts, internal.NewClient, internal.PR, modver.CompareGitWith, modver.CompareDirs)
}

type (
newClientType = func(ctx context.Context, host, token string) (*github.Client, error)
prType = func(ctx context.Context, gh *github.Client, owner, reponame string, prnum int) (modver.Result, error)
compareGitWithType = func(ctx context.Context, repoURL, olderRev, newerRev string, f func(older, newer string) (modver.Result, error)) (modver.Result, error)
compareDirsType = func(older, newer string) (modver.Result, error)
)

func doCompareHelper(ctx context.Context, opts options, newClient newClientType, pr prType, compareGitWith compareGitWithType, compareDirs compareDirsType) (modver.Result, error) {
if opts.pr != "" {
owner, reponame, prnum, err := parsePR(opts.pr)
host, owner, reponame, prnum, err := internal.ParsePR(opts.pr)
if err != nil {
return modver.None, errors.Wrap(err, "parsing pull-request URL")
}
if opts.ghtoken == "" {
return modver.None, fmt.Errorf("usage: %s -pr URL [-token TOKEN]", os.Args[0])
}
gh := github.NewTokenClient(ctx, opts.ghtoken)
return doPR(ctx, gh, owner, reponame, prnum)
gh, err := newClient(ctx, host, opts.ghtoken)
if err != nil {
return modver.None, errors.Wrap(err, "creating GitHub client")
}
return pr(ctx, gh, owner, reponame, prnum)
}

if opts.gitRepo != "" {
Expand All @@ -34,10 +49,10 @@ func doCompare(ctx context.Context, opts options) (modver.Result, error) {
callback = getTags(&opts.v1, &opts.v2, opts.args[0], opts.args[1])
}

return modver.CompareGitWith(ctx, opts.gitRepo, opts.args[0], opts.args[1], callback)
return compareGitWith(ctx, opts.gitRepo, opts.args[0], opts.args[1], callback)
}
if len(opts.args) != 2 {
return nil, fmt.Errorf("usage: %s [-q | -pretty] [-v1 OLDERVERSION -v2 NEWERVERSION] OLDERDIR NEWERDIR", os.Args[0])
}
return modver.CompareDirs(opts.args[0], opts.args[1])
return compareDirs(opts.args[0], opts.args[1])
}
152 changes: 152 additions & 0 deletions cmd/modver/compare_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package main

import (
"context"
"fmt"
"testing"

"github.com/google/go-github/v50/github"

"github.com/bobg/modver/v2"
)

func TestDoCompare(t *testing.T) {
cases := []struct {
opts options
wantErr bool
pr func(*testing.T, *int) prType
compareGitWith func(*testing.T, *int) compareGitWithType
compareDirs func(*testing.T, *int) compareDirsType
}{{
opts: options{
pr: "https://github.com/foo/bar/pull/17",
ghtoken: "token",
},
pr: mockPR("foo", "bar", 17),
}, {
opts: options{
pr: "https://github.com/foo/bar/baz/pull/17",
ghtoken: "token",
},
wantErr: true,
}, {
opts: options{
pr: "https://github.com/foo/bar/pull/17",
},
wantErr: true,
}, {
opts: options{
gitRepo: ".git",
args: []string{"older", "newer"},
},
compareGitWith: mockCompareGitWith(".git", "older", "newer"),
}, {
opts: options{
gitRepo: ".git",
args: []string{"older", "newer", "evenmorenewer"},
},
wantErr: true,
}, {
opts: options{
args: []string{"older", "newer"},
},
compareDirs: mockCompareDirs("older", "newer"),
}, {
opts: options{
args: []string{"older"},
},
wantErr: true,
}}

ctx := context.Background()

for i, tc := range cases {
t.Run(fmt.Sprintf("case_%02d", i+1), func(t *testing.T) {
var (
pr prType
compareGitWith compareGitWithType
compareDirs compareDirsType
calls int
)
if tc.pr != nil {
pr = tc.pr(t, &calls)
}
if tc.compareGitWith != nil {
compareGitWith = tc.compareGitWith(t, &calls)
}
if tc.compareDirs != nil {
compareDirs = tc.compareDirs(t, &calls)
}

_, err := doCompareHelper(ctx, tc.opts, mockNewClient, pr, compareGitWith, compareDirs)
if err != nil {
if !tc.wantErr {
t.Errorf("got error %s, wanted none", err)
}
return
}
if tc.wantErr {
t.Error("got no error, wanted one")
return
}
if calls != 1 {
t.Errorf("got %d calls, want 1", calls)
}
})
}
}

func mockNewClient(ctx context.Context, host, token string) (*github.Client, error) {
return nil, nil
}

func mockPR(wantOwner, wantRepo string, wantPRNum int) func(*testing.T, *int) prType {
return func(t *testing.T, calls *int) prType {
return func(ctx context.Context, gh *github.Client, owner, reponame string, prnum int) (modver.Result, error) {
*calls++
if owner != wantOwner {
t.Errorf("got owner %s, want %s", owner, wantOwner)
}
if reponame != wantRepo {
t.Errorf("got repo %s, want %s", reponame, wantRepo)
}
if wantPRNum != prnum {
t.Errorf("got PR number %d, want %d", prnum, wantPRNum)
}
return modver.None, nil
}
}
}

func mockCompareGitWith(wantGitRepo, wantOlder, wantNewer string) func(*testing.T, *int) compareGitWithType {
return func(t *testing.T, calls *int) compareGitWithType {
return func(ctx context.Context, repoURL, olderRev, newerRev string, f func(older, newer string) (modver.Result, error)) (modver.Result, error) {
*calls++
if repoURL != wantGitRepo {
t.Errorf("got repo URL %s, want %s", repoURL, wantGitRepo)
}
if olderRev != wantOlder {
t.Errorf("got older rev %s, want %s", olderRev, wantOlder)
}
if newerRev != wantNewer {
t.Errorf("got newer rev %s, want %s", newerRev, wantNewer)
}
return modver.None, nil
}
}
}

func mockCompareDirs(wantOlder, wantNewer string) func(*testing.T, *int) compareDirsType {
return func(t *testing.T, calls *int) compareDirsType {
return func(older, newer string) (modver.Result, error) {
*calls++
if older != wantOlder {
t.Errorf("got older dir %s, want %s", older, wantOlder)
}
if newer != wantNewer {
t.Errorf("got newer dir %s, want %s", newer, wantNewer)
}
return modver.None, nil
}
}
}
2 changes: 1 addition & 1 deletion cmd/modver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//
// With `-pr URL`,
// the URL must be that of a github.com pull request
// (having the form https://github.com/OWNER/REPO/pull/NUMBER).
// (having the form https://HOST/OWNER/REPO/pull/NUMBER).
// The environment variable GITHUB_TOKEN must contain a valid GitHub access token,
// or else one must be supplied on the command line with -token.
// In this mode,
Expand Down
Loading

0 comments on commit 9f38d1b

Please sign in to comment.