Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ token in one of the following environment variables:
- GITHUB_TOKEN
- GH_TOKEN

Note that, by default, both of these commands expect the remote branch to already exist. If your
workflow primarily works on *new* branches, you should additionally add the `--branch-from` flag and
supply a commit hash to use as a branch point. With this flag, `commit-headless` will create the
branch on GitHub from that commit hash if it doesn't already exist.

Example: `commit-headless <command> [flags...] --branch-from=$(git rev-parse main HEAD) ...`

In normal usage, `commit-headless` will print *only* the reference to the last commit created on the
remote, allowing this to easily be captured in a script.

Expand Down
5 changes: 5 additions & 0 deletions action-template/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ function main() {
"--branch", process.env.INPUT_BRANCH
];

const branchFrom = process.env["INPUT_BRANCH-FROM"] || "";
if (branchFrom !== "") {
args.push("--branch-from", branchFrom);
}

if (command === "push") {
args.push(...process.env.INPUT_COMMITS.split(/\s+/));
} else {
Expand Down
2 changes: 2 additions & 0 deletions action-template/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ inputs:
branch:
description: 'Target branch name'
required: true
branch-from:
description: 'If necessary, create the remote branch using this commit hash as the branch point.'
command:
description: 'Command to run. One of "commit" or "push"'
required: true
Expand Down
2 changes: 1 addition & 1 deletion cmd_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,5 @@ func (c *CommitCmd) Run() error {

owner, repository := c.Target.Owner(), c.Target.Repository()

return pushChanges(context.Background(), owner, repository, c.Branch, c.DryRun, change)
return pushChanges(context.Background(), owner, repository, c.Branch, c.BranchFrom, c.DryRun, change)
}
2 changes: 1 addition & 1 deletion cmd_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,5 @@ func (c *PushCmd) Run() error {

owner, repository := c.Target.Owner(), c.Target.Repository()

return pushChanges(context.Background(), owner, repository, c.Branch, c.DryRun, changes...)
return pushChanges(context.Background(), owner, repository, c.Branch, c.BranchFrom, c.DryRun, changes...)
}
61 changes: 60 additions & 1 deletion github.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func (c *Client) branchURL() string {
return fmt.Sprintf("%s/repos/%s/%s/branches/%s", c.baseURL, c.owner, c.repo, c.branch)
}

func (c *Client) refsURL() string {
return fmt.Sprintf("%s/repos/%s/%s/git/refs", c.baseURL, c.owner, c.repo)
}

func (c *Client) browseCommitsURL() string {
return fmt.Sprintf("https://github.com/%s/%s/commits/%s", c.owner, c.repo, c.branch)
}
Expand All @@ -57,7 +61,8 @@ func (c *Client) graphqlURL() string {
}

// GetHeadCommitHash returns the current head commit hash for the configured repository and branch
func (c *Client) GetHeadCommitHash(ctx context.Context) (string, error) {
// If the branch does not exist (404 return), we'll attempt to create it from commit branchFrom
func (c *Client) GetHeadCommitHash(ctx context.Context, branchFrom string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.branchURL(), nil)
if err != nil {
return "", fmt.Errorf("prepare http request: %w", err)
Expand All @@ -69,6 +74,14 @@ func (c *Client) GetHeadCommitHash(ctx context.Context) (string, error) {
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotFound {
if branchFrom != "" {
return c.createBranch(ctx, branchFrom)
}

return "", fmt.Errorf("branch %q does not exist on the remote", c.branch)
}

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("get commit hash: http %d", resp.StatusCode)
}
Expand All @@ -86,6 +99,52 @@ func (c *Client) GetHeadCommitHash(ctx context.Context) (string, error) {
return payload.Commit.Sha, nil
}

// createBranch attempts to create c.branch using branchFrom as the branch point
func (c *Client) createBranch(ctx context.Context, branchFrom string) (string, error) {
log("Creating branch from commit %s\n", branchFrom)

var input bytes.Buffer

err := json.NewEncoder(&input).Encode(map[string]string{
"ref": fmt.Sprintf("refs/heads/%s", c.branch),
"sha": branchFrom,
})
if err != nil {
return "", err
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.refsURL(), &input)
if err != nil {
return "", fmt.Errorf("prepare http request: %w", err)
}

resp, err := c.httpC.Do(req)
if err != nil {
return "", fmt.Errorf("create branch request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusUnprocessableEntity {
return "", fmt.Errorf("create branch: http 422 (does the branch point exist?)")
}

if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("create branch: http %d", resp.StatusCode)
}

payload := struct {
Commit struct {
Sha string
} `json:"object"`
}{}

if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return "", fmt.Errorf("decode create branch response: %w", err)
}

return payload.Commit.Sha, nil
}

// PushChanges takes a list of changes and a commit hash and produces commits using the GitHub GraphQL API.
// The commit hash is expected to be the current head of the remote branch, see [GetHeadCommitHash]
// for more.
Expand Down
7 changes: 4 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ func (f targetFlag) Repository() string {

// flags that are shared among commands that interact with the remote
type remoteFlags struct {
Target targetFlag `name:"target" short:"T" required:"" help:"Target repository in owner/repo format."`
Branch string `required:"" help:"Name of the target branch on the remote."`
DryRun bool `name:"dry-run" help:"Perform everything except the final remote writes to GitHub."`
Target targetFlag `name:"target" short:"T" required:"" help:"Target repository in owner/repo format."`
Branch string `required:"" help:"Name of the target branch on the remote."`
BranchFrom string `help:"If necessary, create the remote branch using this commit sha."`
DryRun bool `name:"dry-run" help:"Perform everything except the final remote writes to GitHub."`
}

type CLI struct {
Expand Down
8 changes: 6 additions & 2 deletions pushchanges.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

// Takes a list of changes to push to the remote identified by target.
// Prints the last commit pushed to standard output.
func pushChanges(ctx context.Context, owner, repository, branch string, dryrun bool, changes ...Change) error {
func pushChanges(ctx context.Context, owner, repository, branch, branchFrom string, dryrun bool, changes ...Change) error {
hashes := []string{}
for i := 0; i < len(changes) && i < 10; i++ {
hashes = append(hashes, changes[i].hash)
Expand All @@ -25,6 +25,10 @@ func pushChanges(ctx context.Context, owner, repository, branch string, dryrun b
log("Branch: %s\n", branch)
log("Commits: %s\n", strings.Join(hashes, ", "))

if branchFrom != "" && (!hashRegex.MatchString(branchFrom) || len(branchFrom) != 40) {
return fmt.Errorf("cannot branch from %q, must be a full 40 hex digit commit hash", branchFrom)
}

token := getToken(os.Getenv)
if token == "" {
return errors.New("no GitHub token supplied")
Expand All @@ -33,7 +37,7 @@ func pushChanges(ctx context.Context, owner, repository, branch string, dryrun b
client := NewClient(ctx, token, owner, repository, branch)
client.dryrun = dryrun

headRef, err := client.GetHeadCommitHash(context.Background())
headRef, err := client.GetHeadCommitHash(context.Background(), branchFrom)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion version.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package main

const VERSION = "0.4.0"
const VERSION = "0.5.0"