diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4c16b7c..31e5711e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -197,15 +197,25 @@ jobs: - run: tar xzf artifacts.tgz working-directory: ${{ steps.tea.outputs.prefix }} + - run: | + tea +gnupg.org gpg-agent --daemon || true + echo $GPG_PRIVATE_KEY | \ + base64 -d | \ + tea +gnupg.org gpg --import --batch --yes + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + - run: scripts/bottle.ts ${{ needs.build.outputs.built }} id: bottle-xz env: COMPRESSION: xz + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} - run: scripts/bottle.ts ${{ needs.build.outputs.built }} id: bottle-gz env: COMPRESSION: gz + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} - name: upload bottles id: upload @@ -214,6 +224,7 @@ jobs: --srcs ${{ needs.build.outputs.srcs }} ${{ needs.build.outputs.srcs }} --bottles ${{ steps.bottle-gz.outputs.bottles }} ${{ steps.bottle-xz.outputs.bottles }} --checksums ${{ steps.bottle-gz.outputs.checksums }} ${{ steps.bottle-xz.outputs.checksums }} + --signatures ${{ steps.bottle-gz.outputs.signatures }} ${{ steps.bottle-xz.outputs.signatures }} env: AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/scripts/bottle.ts b/scripts/bottle.ts index dd711909..132ff9ac 100755 --- a/scripts/bottle.ts +++ b/scripts/bottle.ts @@ -5,6 +5,7 @@ dependencies: gnu.org/tar: ^1.34 tukaani.org/xz: ^5 zlib.net: 1 + gnupg.org: ^2 args: - deno - run @@ -18,9 +19,10 @@ args: import { Installation } from "types" import { useCellar, usePrefix, useFlags, useCache } from "hooks" -import { run } from "utils" +import { backticks, run } from "utils" import { crypto } from "deno/crypto/mod.ts" import { encode } from "deno/encoding/hex.ts" +import { encode as base64Encode } from "deno/encoding/base64.ts" import { set_output } from "./utils/gha.ts" import * as ARGV from "./utils/args.ts" import Path from "path" @@ -34,7 +36,11 @@ if (import.meta.main) { useFlags() const compression = Deno.env.get("COMPRESSION") == 'xz' ? 'xz' : 'gz' + const gpgKey = Deno.env.get("GPG_KEY_ID") + const gpgPassphrase = Deno.env.get("GPG_PASSPHRASE") + if (!gpgKey || !gpgPassphrase) throw new Error("missing GPG_KEY_ID") const checksums: string[] = [] + const signatures: string[] = [] const bottles: Path[] = [] for await (const pkg of ARGV.pkgs()) { @@ -43,15 +49,18 @@ if (import.meta.main) { const installation = await cellar.resolve(pkg) const path = await bottle(installation, compression) const checksum = await sha256(path) + const signature = await gpg(path, { gpgKey, gpgPassphrase }) console.log({ bottled: path }) bottles.push(path) checksums.push(checksum) + signatures.push(signature) } await set_output("bottles", bottles) await set_output("checksums", checksums) + await set_output("signatures", signatures) } @@ -70,3 +79,26 @@ export async function sha256(file: Path): Promise { .then(file => crypto.subtle.digest("SHA-256", file.readable)) .then(buf => new TextDecoder().decode(encode(new Uint8Array(buf)))) } + +interface GPGCredentials { + gpgKey: string + gpgPassphrase: string +} + +async function gpg(file: Path, { gpgKey, gpgPassphrase }: GPGCredentials): Promise { + const rv = await backticks({ + cmd: [ + "gpg", + "--detach-sign", + "--armor", + "--output", + "-", + "--local-user", + gpgKey, + "--passphrase", + gpgPassphrase, + file.string + ] + }) + return base64Encode(rv) +} \ No newline at end of file diff --git a/scripts/upload.ts b/scripts/upload.ts index a20119ed..1d8fd738 100755 --- a/scripts/upload.ts +++ b/scripts/upload.ts @@ -16,6 +16,7 @@ import { useCache, useFlags, useOffLicense, usePrefix } from "hooks" import { Package, PackageRequirement } from "types" import SemVer, * as semver from "semver" import { basename, dirname } from "deno/path/mod.ts" +import { decode as base64Decode } from "deno/encoding/base64.ts" import Path from "path" import { set_output } from "./utils/gha.ts" import { sha256 } from "./bottle.ts" @@ -101,12 +102,14 @@ const pkgs = args_get("pkgs").map(pkgutils.parse).map(assert_pkg) const srcs = args_get("srcs") const bottles = args_get("bottles") const checksums = args_get("checksums") +const signatures = args_get("signatures") const rv: string[] = [] for (const [index, pkg] of pkgs.entries()) { const bottle = new Path(bottles[index]) const checksum = checksums[index] + const signature = base64Decode(signatures[index]) const stowed = cache.decode(bottle)! const key = useOffLicense("s3").key(stowed) const versions = await get_versions(key, pkg, bucket) @@ -114,6 +117,7 @@ for (const [index, pkg] of pkgs.entries()) { //FIXME stream the bottle (at least) to S3 await put(key, bottle, bucket) await put(`${key}.sha256sum`, `${checksum} ${basename(key)}`, bucket) + await put(`${key}.asc`, signature, bucket) await put(`${dirname(key)}/versions.txt`, versions.join("\n"), bucket) // mirror the sources