hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit 5fc1b70abe2624274d73c46bec114cc97c623e38
parent 02a0efd03fb7367d31db7e8d662feebb02e68df7
Author: Drew DeVault <sir@cmpwn.com>
Date:   Sun, 21 Nov 2021 18:10:23 +0100

hare release: add git note signature

Signed-off-by: Drew DeVault <sir@cmpwn.com>

Diffstat:
Mcmd/hare/release.ha | 106++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 100 insertions(+), 6 deletions(-)

diff --git a/cmd/hare/release.ha b/cmd/hare/release.ha @@ -1,9 +1,11 @@ +use bufio; use errors; use fmt; use fs; use io; use os::exec; use os; +use path; use strconv; use strings; use temp; @@ -21,6 +23,20 @@ type badversion = !void; type release_error = !(exec::error | io::error | fs::error | errors::error | badversion | git_error); +const changelog_template: str = "# This is the changelog for your release. It has been automatically pre-filled +# with the changes from version {} via git-shortlog(1). Please review the +# changelog below and, if necessary, add a brief note regarding any steps +# required for the user to upgrade this software. It is recommended to keep this +# brief and clinical, so readers can quickly understand what's changed, and to +# save marketing comments for a separate release announcement. +# +# Any lines which begin with \"#\", like this one, are for your information +# only, and will be removed from the final changelog. Edit this file to your +# satisfaction, then save and close your editor. +# +{} version {} +"; + fn parseversion(in: str) (modversion | badversion) = { const items = strings::split(in, "."); defer free(items); @@ -46,15 +62,15 @@ fn do_release(incr: increment, dryrun: bool) (void | release_error) = { // changes (major bump), or neither (patch bump). I don't feel that // fancy, however. - // TODO: - // - Run hare test - // - Run git tag -a with release notes pre-filled - // - Generate & sign tarballs as git notes + // TODO: Run hare test checkbranch()?; checkstatus()?; git_runcmd("fetch")?; checkbehind()?; + const key = choosekey()?; + defer free(key); + // TODO: Detect if distance from the last tag is zero commits const lasttag = match (git_readcmd("describe", "--abbrev=0")) { case git_error => @@ -80,15 +96,46 @@ fn do_release(incr: increment, dryrun: bool) (void | release_error) = { const range = fmt::asprintf("{}..HEAD", lasttag); defer free(range); + const name = path::basename(os::getcwd()); const dir = temp::dir(); defer os::rmdirall(dir)!; const changelog = temp::named(os::cwd, dir, io::mode::WRITE)?; const clfile = changelog.0, changelog = changelog.1; - fmt::fprintfln(clfile, "# Version {}", newtag)?; - fmt::fprintln(clfile, "# TODO: Fill in more stuff here")?; + fmt::fprintfln(clfile, changelog_template, lasttag, name, newtag)?; shortlog(clfile, range)?; git_runcmd("tag", "-aeF", changelog, newtag)?; + + fmt::printfln("Using SSH key {} for release signature.", key)!; + const prefix = fmt::asprintf("--prefix={}-{}/", name, newtag); + defer free(prefix); + const archive = exec::cmd("git", "archive", + "--format=tar.gz", prefix, newtag)?; + const ssh = exec::cmd("ssh-keygen", + "-Y", "sign", "-f", key, "-n", "file")?; + const note = exec::cmd("git", "notes", "add", "-F", "-", newtag)?; + exec::setenv(&note, "GIT_NOTES_REF", "refs/notes/signatures/tar.gz"); + + const pipe1 = unix::pipe()?; + const pipe2 = unix::pipe()?; + exec::addfile(&archive, pipe1.1, os::stdout_file); + exec::addfile(&ssh, pipe1.0, os::stdin_file); + exec::addfile(&ssh, pipe2.1, os::stdout_file); + exec::addfile(&note, pipe2.0, os::stdin_file); + const archive = exec::start(&archive)?; + const ssh = exec::start(&ssh)?; + const note = exec::start(&note)?; + io::close(pipe1.0); + io::close(pipe1.1); + io::close(pipe2.0); + io::close(pipe2.1); + exec::check(&exec::wait(&archive)?)?; + exec::check(&exec::wait(&ssh)?)?; + exec::check(&exec::wait(&note)?)?; + + fmt::printfln("Tagged {} version {}. " + "Use 'git push --follow-tags' to publish the new release.", + name, newtag)!; }; fn do_initial_release() (void | release_error) = { @@ -149,6 +196,53 @@ fn shortlog(out: io::file, what: str) (void | release_error) = { exec::check(&status)?; }; +fn choosekey() (str | release_error) = { + match (os::getenv("HAREKEY")) { + case void => void; + case name: str => + return name; + }; + const paths = [ + "id_ed25519", + "id_ecdsa", + "id_rsa", + "id_dsa", + ]; + const home = os::getenv("HOME") as str; + for (let i = 0z; i < len(paths); i += 1) { + const cand = path::join(home, ".ssh", paths[i]); + if (os::stat(cand) is fs::error) { + free(cand); + continue; + }; + return cand; + }; + fmt::errorln("No suitable SSH key found to sign releases with.")!; + + fmt::error("Would you like to generate one now? [Y/n] ")!; + const line = match (bufio::scanline(os::stdin)?) { + case io::EOF => + fmt::fatal("No suitable key available. Terminating."); + case line: []u8 => + yield strings::fromutf8(line); + }; + defer free(line); + if (line != "" && line != "y" && line != "Y") { + fmt::fatal("No suitable key available. Terminating."); + }; + + const parent = path::join(home, ".ssh"); + defer free(parent); + os::mkdirs(parent)?; + + const path = path::join(home, ".ssh", "id_ed25519"); + const cmd = exec::cmd("ssh-keygen", "-t", "ed25519", "-f", path)?; + const proc = exec::start(&cmd)?; + const status = exec::wait(&proc)?; + exec::check(&status)?; + return path; +}; + fn git_runcmd(args: str...) (void | release_error) = { const cmd = exec::cmd("git", args...)?; const proc = exec::start(&cmd)?;