From 38c1f188caf104ce9b6fe1e46eabc1044f698b33 Mon Sep 17 00:00:00 2001
From: CrazyMax <crazy-max@users.noreply.github.com>
Date: Mon, 17 Aug 2020 22:18:15 +0200
Subject: [PATCH] Add digest output Fix platforms and allow inputs

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
---
 .github/workflows/ci.yml | 49 +++++++++++++++++++++++-
 README.md                | 52 +++++++++++++++++++++++++-
 __tests__/buildx.test.ts | 15 ++++++++
 action.yml               | 16 ++++----
 dist/index.js            | 80 +++++++++++++++++++++++++++++-----------
 src/buildx.ts            | 15 ++++++++
 src/context.ts           | 78 +++++++++++++++++++++------------------
 src/main.ts              |  7 ++++
 8 files changed, 244 insertions(+), 68 deletions(-)
 create mode 100644 __tests__/buildx.test.ts

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a031acb..411ae0b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,54 @@ on:
       - v2-working-branch # remove when merged to master
 
 jobs:
-  main:
+  single:
+    runs-on: ubuntu-latest
+    steps:
+      -
+        name: Run local registry
+        run: |
+          docker run -d -p 5000:5000 registry:2
+      -
+        name: Checkout
+        uses: actions/checkout@v2.3.1
+      -
+        name: Set up QEMU
+        uses: ./setup-qemu/ # change to docker/setup-qemu-action@master
+        with:
+          platforms: all
+      -
+        name: Set up Docker Buildx
+        id: buildx
+        uses: ./setup-buildx/ # change to docker/setup-buildx-action@master
+        with:
+          driver-opt: network=host
+          buildkitd-flags: --allow-insecure-entitlement security.insecure
+      -
+        name: Build and push
+        id: docker_build
+        uses: ./
+        with:
+          context: ./test
+          file: ./test/Dockerfile
+          builder: ${{ steps.buildx.outputs.name }}
+          allow: network.host,security.insecure
+          push: true
+          tags: |
+            localhost:5000/name/app:latest
+            localhost:5000/name/app:1.0.0
+      -
+        name: Inspect
+        run: |
+          docker buildx imagetools inspect localhost:5000/name/app:1.0.0
+      -
+        name: Image digest
+        run: echo ${{ steps.docker_build.outputs.digest }}
+      -
+        name: Dump context
+        if: always()
+        uses: crazy-max/ghaction-dump-context@v1
+
+  multi:
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
diff --git a/README.md b/README.md
index 808f5fb..c522d0b 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,56 @@ on:
     tags:
 
 jobs:
-  buildx:
+  main:
+    runs-on: ubuntu-latest
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v2
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v1
+        with:
+          platforms: all
+      -
+        name: Set up Docker Buildx
+        id: buildx
+        uses: docker/setup-buildx-action@v1
+      -
+        name: Login to DockerHub
+        uses: crazy-max/ghaction-docker-login@v1 # switch to docker/login-action@v1 when available 
+        with:
+          username: ${{ secrets.DOCKER_USERNAME }}
+          password: ${{ secrets.DOCKER_PASSWORD }}
+      -
+        name: Build and push
+        id: docker_build
+        uses: docker/build-push-action@v2
+        with:
+          builder: ${{ steps.buildx.outputs.name }}
+          push: true
+          tags: |
+            user/app:latest
+            user/app:1.0.0
+      -
+        name: Image digest
+        run: echo ${{ steps.docker_build.outputs.digest }}
+```
+
+### Multi-platform image
+
+```yaml
+name: ci
+
+on:
+  pull_request:
+    branches: master
+  push:
+    branches: master
+    tags:
+
+jobs:
+  multi:
     runs-on: ubuntu-latest
     steps:
       -
@@ -58,7 +107,6 @@ jobs:
         uses: docker/build-push-action@v2
         with:
           builder: ${{ steps.buildx.outputs.name }}
-          platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le,linux/s390x
           push: true
           tags: |
             user/app:latest
diff --git a/__tests__/buildx.test.ts b/__tests__/buildx.test.ts
new file mode 100644
index 0000000..7e05f5e
--- /dev/null
+++ b/__tests__/buildx.test.ts
@@ -0,0 +1,15 @@
+import fs from 'fs';
+import * as buildx from '../src/buildx';
+
+const digest = 'sha256:bfb45ab72e46908183546477a08f8867fc40cebadd00af54b071b097aed127a9';
+
+describe('getImageID', () => {
+  it('matches', async () => {
+    const imageIDFile = await buildx.getImageIDFile();
+    console.log(`imageIDFile: ${imageIDFile}`);
+    await fs.writeFileSync(imageIDFile, digest);
+    const imageID = await buildx.getImageID();
+    console.log(`imageID: ${imageID}`);
+    expect(imageID).toEqual(digest);
+  });
+});
diff --git a/action.yml b/action.yml
index 8bd6603..42adf66 100644
--- a/action.yml
+++ b/action.yml
@@ -19,13 +19,13 @@ inputs:
     required: false
     default: './Dockerfile'
   build-args:
-    description: "Newline-delimited list of build-time variables"
+    description: "List of build-time variables"
     required: false
   labels:
-    description: "Newline-delimited list of metadata for an image"
+    description: "List of metadata for an image"
     required: false
   tags:
-    description: "Newline-delimited list of tags"
+    description: "List of tags"
     required: false
   pull:
     description: "Always attempt to pull a newer version of the image"
@@ -35,14 +35,14 @@ inputs:
     description: "Sets the target stage to build"
     required: false
   allow:
-    description: "Allow extra privileged entitlement (eg. network.host,security.insecure)"
+    description: "List of extra privileged entitlement (eg. network.host,security.insecure)"
     required: false
   no-cache:
     description: "Do not use cache when building the image"
     required: false
     default: 'false'
   platforms:
-    description: "Comma-delimited list of target platforms for build"
+    description: "List of target platforms for build"
     required: false
   load:
     description: "Load is a shorthand for --output=type=docker"
@@ -53,13 +53,13 @@ inputs:
     required: false
     default: 'false'
   outputs:
-    description: "Newline-delimited list of output destinations (format: type=local,dest=path)"
+    description: "List of output destinations (format: type=local,dest=path)"
     required: false
   cache-from:
-    description: "Newline-delimited list of external cache sources for buildx (eg. user/app:cache, type=local,src=path/to/dir)"
+    description: "List of external cache sources for buildx (eg. user/app:cache, type=local,src=path/to/dir)"
     required: false
   cache-to:
-    description: "Newline-delimited list of cache export destinations for buildx (eg. user/app:cache, type=local,dest=path/to/dir)"
+    description: "List of cache export destinations for buildx (eg. user/app:cache, type=local,dest=path/to/dir)"
     required: false
 
 outputs:
diff --git a/dist/index.js b/dist/index.js
index 44b2147..cb6eabe 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1025,6 +1025,12 @@ function run() {
             core.info(`🏃 Starting build...`);
             const args = yield context_1.getArgs(inputs);
             yield exec.exec('docker', args);
+            const imageID = yield buildx.getImageID();
+            if (imageID) {
+                core.info('🛒 Extracting digest...');
+                core.info(`${imageID}`);
+                core.setOutput('digest', imageID);
+            }
         }
         catch (error) {
             core.setFailed(error.message);
@@ -1405,8 +1411,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
     });
 };
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.asyncForEach = exports.getInputList = exports.getArgs = exports.getInputs = void 0;
+exports.asyncForEach = exports.getInputList = exports.getArgs = exports.getInputs = exports.tmpDir = void 0;
+const fs = __importStar(__webpack_require__(747));
+const os = __importStar(__webpack_require__(87));
+const path = __importStar(__webpack_require__(622));
+const buildx = __importStar(__webpack_require__(982));
 const core = __importStar(__webpack_require__(470));
+exports.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-build-push-'));
 function getInputs() {
     return __awaiter(this, void 0, void 0, function* () {
         return {
@@ -1440,24 +1451,6 @@ function getArgs(inputs) {
     });
 }
 exports.getArgs = getArgs;
-function getCommonArgs(inputs) {
-    return __awaiter(this, void 0, void 0, function* () {
-        let args = [];
-        if (inputs.noCache) {
-            args.push('--no-cache');
-        }
-        if (inputs.pull) {
-            args.push('--pull');
-        }
-        if (inputs.load) {
-            args.push('--load');
-        }
-        if (inputs.push) {
-            args.push('--push');
-        }
-        return args;
-    });
-}
 function getBuildArgs(inputs) {
     return __awaiter(this, void 0, void 0, function* () {
         let args = ['build'];
@@ -1473,12 +1466,15 @@ function getBuildArgs(inputs) {
         if (inputs.target) {
             args.push('--target', inputs.target);
         }
-        if (inputs.allow) {
+        if (inputs.allow.length > 0) {
             args.push('--allow', inputs.allow.join(','));
         }
-        if (inputs.platforms) {
+        if (inputs.platforms.length > 0) {
             args.push('--platform', inputs.platforms.join(','));
         }
+        else {
+            args.push('--iidfile', yield buildx.getImageIDFile());
+        }
         yield exports.asyncForEach(inputs.outputs, (output) => __awaiter(this, void 0, void 0, function* () {
             args.push('--output', output);
         }));
@@ -1494,6 +1490,24 @@ function getBuildArgs(inputs) {
         return args;
     });
 }
+function getCommonArgs(inputs) {
+    return __awaiter(this, void 0, void 0, function* () {
+        let args = [];
+        if (inputs.noCache) {
+            args.push('--no-cache');
+        }
+        if (inputs.pull) {
+            args.push('--pull');
+        }
+        if (inputs.load) {
+            args.push('--load');
+        }
+        if (inputs.push) {
+            args.push('--push');
+        }
+        return args;
+    });
+}
 function getInputList(name) {
     return __awaiter(this, void 0, void 0, function* () {
         const items = core.getInput(name);
@@ -1838,9 +1852,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
         step((generator = generator.apply(thisArg, _arguments || [])).next());
     });
 };
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.use = exports.isAvailable = void 0;
+exports.use = exports.isAvailable = exports.getImageID = exports.getImageIDFile = void 0;
+const fs_1 = __importDefault(__webpack_require__(747));
+const path_1 = __importDefault(__webpack_require__(622));
+const context = __importStar(__webpack_require__(482));
 const exec = __importStar(__webpack_require__(807));
+function getImageIDFile() {
+    return __awaiter(this, void 0, void 0, function* () {
+        return path_1.default.join(context.tmpDir, 'iidfile');
+    });
+}
+exports.getImageIDFile = getImageIDFile;
+function getImageID() {
+    return __awaiter(this, void 0, void 0, function* () {
+        const iidFile = yield getImageIDFile();
+        if (!fs_1.default.existsSync(iidFile)) {
+            return undefined;
+        }
+        return fs_1.default.readFileSync(iidFile, { encoding: 'utf-8' });
+    });
+}
+exports.getImageID = getImageID;
 function isAvailable() {
     return __awaiter(this, void 0, void 0, function* () {
         return yield exec.exec(`docker`, ['buildx'], true).then(res => {
diff --git a/src/buildx.ts b/src/buildx.ts
index 38f38ef..0574cd5 100644
--- a/src/buildx.ts
+++ b/src/buildx.ts
@@ -1,5 +1,20 @@
+import fs from 'fs';
+import path from 'path';
+import * as context from './context';
 import * as exec from './exec';
 
+export async function getImageIDFile(): Promise<string> {
+  return path.join(context.tmpDir, 'iidfile');
+}
+
+export async function getImageID(): Promise<string | undefined> {
+  const iidFile = await getImageIDFile();
+  if (!fs.existsSync(iidFile)) {
+    return undefined;
+  }
+  return fs.readFileSync(iidFile, {encoding: 'utf-8'});
+}
+
 export async function isAvailable(): Promise<Boolean> {
   return await exec.exec(`docker`, ['buildx'], true).then(res => {
     if (res.stderr != '' && !res.success) {
diff --git a/src/context.ts b/src/context.ts
index 2d8a6d7..2c55af5 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -1,5 +1,11 @@
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import * as buildx from './buildx';
 import * as core from '@actions/core';
 
+export const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-build-push-'));
+
 export interface Inputs {
   context: string;
   file: string;
@@ -48,6 +54,43 @@ export async function getArgs(inputs: Inputs): Promise<Array<string>> {
   return args;
 }
 
+async function getBuildArgs(inputs: Inputs): Promise<Array<string>> {
+  let args: Array<string> = ['build'];
+  await asyncForEach(inputs.buildArgs, async buildArg => {
+    args.push('--build-arg', buildArg);
+  });
+  await asyncForEach(inputs.labels, async label => {
+    args.push('--label', label);
+  });
+  await asyncForEach(inputs.tags, async tag => {
+    args.push('--tag', tag);
+  });
+  if (inputs.target) {
+    args.push('--target', inputs.target);
+  }
+  if (inputs.allow.length > 0) {
+    args.push('--allow', inputs.allow.join(','));
+  }
+  if (inputs.platforms.length > 0) {
+    args.push('--platform', inputs.platforms.join(','));
+  } else {
+    args.push('--iidfile', await buildx.getImageIDFile());
+  }
+  await asyncForEach(inputs.outputs, async output => {
+    args.push('--output', output);
+  });
+  await asyncForEach(inputs.cacheFrom, async cacheFrom => {
+    args.push('--cache-from', cacheFrom);
+  });
+  await asyncForEach(inputs.cacheTo, async cacheTo => {
+    args.push('--cache-to', cacheTo);
+  });
+  if (inputs.file) {
+    args.push('--file', inputs.file);
+  }
+  return args;
+}
+
 async function getCommonArgs(inputs: Inputs): Promise<Array<string>> {
   let args: Array<string> = [];
   if (inputs.noCache) {
@@ -65,41 +108,6 @@ async function getCommonArgs(inputs: Inputs): Promise<Array<string>> {
   return args;
 }
 
-async function getBuildArgs(inputs: Inputs): Promise<Array<string>> {
-  let args: Array<string> = ['build'];
-  await asyncForEach(inputs.buildArgs, async buildArg => {
-    args.push('--build-arg', buildArg);
-  });
-  await asyncForEach(inputs.labels, async label => {
-    args.push('--label', label);
-  });
-  await asyncForEach(inputs.tags, async tag => {
-    args.push('--tag', tag);
-  });
-  if (inputs.target) {
-    args.push('--target', inputs.target);
-  }
-  if (inputs.allow) {
-    args.push('--allow', inputs.allow.join(','));
-  }
-  if (inputs.platforms) {
-    args.push('--platform', inputs.platforms.join(','));
-  }
-  await asyncForEach(inputs.outputs, async output => {
-    args.push('--output', output);
-  });
-  await asyncForEach(inputs.cacheFrom, async cacheFrom => {
-    args.push('--cache-from', cacheFrom);
-  });
-  await asyncForEach(inputs.cacheTo, async cacheTo => {
-    args.push('--cache-to', cacheTo);
-  });
-  if (inputs.file) {
-    args.push('--file', inputs.file);
-  }
-  return args;
-}
-
 export async function getInputList(name: string): Promise<string[]> {
   const items = core.getInput(name);
   if (items == '') {
diff --git a/src/main.ts b/src/main.ts
index 67732e5..ee27c94 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -25,6 +25,13 @@ async function run(): Promise<void> {
     core.info(`🏃 Starting build...`);
     const args: string[] = await getArgs(inputs);
     await exec.exec('docker', args);
+
+    const imageID = await buildx.getImageID();
+    if (imageID) {
+      core.info('🛒 Extracting digest...');
+      core.info(`${imageID}`);
+      core.setOutput('digest', imageID);
+    }
   } catch (error) {
     core.setFailed(error.message);
   }