From fc583d3c8661f163fcc5a28fee67df2783cf6637 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 7 Jul 2020 21:51:23 -0300 Subject: [PATCH] feat(action) add option to upload release assets --- action.yml | 7 +- dist/main.js | 12 ++- dist/upload-release-assets.js | 65 +++++++++++++++ package.json | 1 + src/main.ts | 14 +++- src/upload-release-assets.ts | 33 ++++++++ yarn.lock | 150 +++++++++++++++++++++++++++++++++- 7 files changed, 273 insertions(+), 9 deletions(-) create mode 100644 dist/upload-release-assets.js create mode 100644 src/upload-release-assets.ts diff --git a/action.yml b/action.yml index f778b83..6874cbf 100644 --- a/action.yml +++ b/action.yml @@ -2,6 +2,10 @@ name: 'Your name here' description: 'Provide a description here' author: 'Lucas Nogueira ' inputs: + releaseId: + description: 'The id of the release to upload the assets' + uploadUrl: + description: 'The URL for uploading assets to the release' projectPath: description: 'path to the root of the project that will be built' default: '.' @@ -10,9 +14,6 @@ inputs: default: 'tauri.conf.json' distPath: description: 'path to the distributable folder with your index.html and JS/CSS' -outputs: - bundlePath: - description: 'path to the build artifact' runs: using: 'node12' main: 'dist/main.js' diff --git a/dist/main.js b/dist/main.js index 1f8d6dc..f1dbd0c 100644 --- a/dist/main.js +++ b/dist/main.js @@ -36,6 +36,7 @@ const core = __importStar(require("@actions/core")); const execa_1 = __importDefault(require("execa")); const path_1 = require("path"); const fs_1 = require("fs"); +const upload_release_assets_1 = __importDefault(require("./upload-release-assets")); function hasTauriDependency(root) { const packageJsonPath = path_1.join(root, 'package.json'); if (fs_1.existsSync(packageJsonPath)) { @@ -119,13 +120,20 @@ function run() { const projectPath = core.getInput('projectPath') || process.argv[2]; const configPath = path_1.join(projectPath, core.getInput('configPath') || 'tauri.conf.json'); const distPath = core.getInput('distPath'); + const uploadUrl = core.getInput('uploadUrl'); + const releaseId = core.getInput('releaseId'); + if ((!!uploadUrl) !== (!!releaseId)) { + core.setFailed('To upload artifacts to a release, you need to set both `releaseId` and `uploadUrl`.'); + return; + } let config = null; if (fs_1.existsSync(configPath)) { config = JSON.parse(fs_1.readFileSync(configPath).toString()); } const artifacts = yield buildProject(projectPath, [], { configPath: config, distPath }); - console.log(`artifacts: ${artifacts}`); - core.setOutput('artifacts', artifacts); + if (uploadUrl && releaseId) { + yield upload_release_assets_1.default(uploadUrl, Number(releaseId), artifacts); + } } catch (error) { core.setFailed(error.message); diff --git a/dist/upload-release-assets.js b/dist/upload-release-assets.js new file mode 100644 index 0000000..71df5e5 --- /dev/null +++ b/dist/upload-release-assets.js @@ -0,0 +1,65 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + 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 }); +const core = __importStar(require("@actions/core")); +const github_1 = require("@actions/github"); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +function uploadAssets(uploadUrl, releaseId, assets) { + return __awaiter(this, void 0, void 0, function* () { + try { + if (process.env.GITHUB_TOKEN === undefined) { + throw new Error('GITHUB_TOKEN is required'); + } + const github = github_1.getOctokit(process.env.GITHUB_TOKEN); + // Determine content-length for header to upload asset + const contentLength = (filePath) => fs_1.default.statSync(filePath).size; + for (const assetPath of assets) { + const headers = { 'content-type': 'application/zip', 'content-length': contentLength(assetPath) }; + yield github.repos.uploadReleaseAsset({ + url: uploadUrl, + headers, + name: path_1.default.basename(assetPath), + data: fs_1.default.readFileSync(assetPath).toString(), + owner: github_1.context.repo.owner, + repo: github_1.context.repo.repo, + release_id: Number(releaseId) + }); + } + } + catch (error) { + core.setFailed(error.message); + } + }); +} +exports.default = uploadAssets; diff --git a/package.json b/package.json index 4da01cf..b00fc1e 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "license": "MIT", "dependencies": { "@actions/core": "^1.2.0", + "@actions/github": "^4.0.0", "execa": "^4.0.3" }, "devDependencies": { diff --git a/src/main.ts b/src/main.ts index 0e66e6f..2cd8cfa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import * as core from '@actions/core' import execa from 'execa' import { join } from 'path' import { readFileSync, existsSync, copyFileSync, writeFileSync } from 'fs' +import uploadReleaseAssets from './upload-release-assets' function hasTauriDependency(root: string): boolean { const packageJsonPath = join(root, 'package.json') @@ -89,6 +90,13 @@ async function run(): Promise { const projectPath = core.getInput('projectPath') || process.argv[2] const configPath = join(projectPath, core.getInput('configPath') || 'tauri.conf.json') const distPath = core.getInput('distPath') + const uploadUrl = core.getInput('uploadUrl') + const releaseId = core.getInput('releaseId') + + if ((!!uploadUrl) !== (!!releaseId)) { + core.setFailed('To upload artifacts to a release, you need to set both `releaseId` and `uploadUrl`.') + return + } let config = null if (existsSync(configPath)) { @@ -96,8 +104,10 @@ async function run(): Promise { } const artifacts = await buildProject(projectPath, [], { configPath: config, distPath }) - console.log(`artifacts: ${artifacts}`) - core.setOutput('artifacts', artifacts) + + if (uploadUrl && releaseId) { + await uploadReleaseAssets(uploadUrl, Number(releaseId), artifacts) + } } catch (error) { core.setFailed(error.message) } diff --git a/src/upload-release-assets.ts b/src/upload-release-assets.ts new file mode 100644 index 0000000..80d3453 --- /dev/null +++ b/src/upload-release-assets.ts @@ -0,0 +1,33 @@ +import * as core from '@actions/core' +import { getOctokit, context } from '@actions/github' +import fs from 'fs' +import path from 'path' + +export default async function uploadAssets(uploadUrl: string, releaseId: number, assets: string[]) { + try { + if (process.env.GITHUB_TOKEN === undefined) { + throw new Error('GITHUB_TOKEN is required') + } + + const github = getOctokit(process.env.GITHUB_TOKEN) + + // Determine content-length for header to upload asset + const contentLength = (filePath: string) => fs.statSync(filePath).size + + for (const assetPath of assets) { + const headers = { 'content-type': 'application/zip', 'content-length': contentLength(assetPath) } + + await github.repos.uploadReleaseAsset({ + url: uploadUrl, + headers, + name: path.basename(assetPath), + data: fs.readFileSync(assetPath).toString(), + owner: context.repo.owner, + repo: context.repo.repo, + release_id: Number(releaseId) + }) + } + } catch (error) { + core.setFailed(error.message) + } +} diff --git a/yarn.lock b/yarn.lock index e798089..05f5308 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,23 @@ resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.2.4.tgz#96179dbf9f8d951dd74b40a0dbd5c22555d186ab" integrity sha512-YJCEq8BE3CdN8+7HPZ/4DxJjk/OkZV2FFIf+DlZTC/4iBlzYCD5yjRR6eiOS5llO11zbRltIRuKAjMKaWTE6cg== +"@actions/github@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@actions/github/-/github-4.0.0.tgz#d520483151a2bf5d2dc9cd0f20f9ac3a2e458816" + integrity sha512-Ej/Y2E+VV6sR9X7pWL5F3VgEWrABaT292DRqRU6R4hnQjPtC/zD3nagxVdXWiRQvYDh8kHXo7IDmG42eJ/dOMA== + dependencies: + "@actions/http-client" "^1.0.8" + "@octokit/core" "^3.0.0" + "@octokit/plugin-paginate-rest" "^2.2.3" + "@octokit/plugin-rest-endpoint-methods" "^4.0.0" + +"@actions/http-client@^1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-1.0.8.tgz#8bd76e8eca89dc8bcf619aa128eba85f7a39af45" + integrity sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA== + dependencies: + tunnel "0.0.6" + "@ardatan/aggregate-error@0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@ardatan/aggregate-error/-/aggregate-error-0.0.1.tgz#1403ac5de10d8ca689fc1f65844c27179ae1d44f" @@ -494,6 +511,88 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@octokit/auth-token@^2.4.0": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.2.tgz#10d0ae979b100fa6b72fa0e8e63e27e6d0dbff8a" + integrity sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ== + dependencies: + "@octokit/types" "^5.0.0" + +"@octokit/core@^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.1.0.tgz#9c3c9b23f7504668cfa057f143ccbf0c645a0ac9" + integrity sha512-yPyQSmxIXLieEIRikk2w8AEtWkFdfG/LXcw1KvEtK3iP0ENZLW/WYQmdzOKqfSaLhooz4CJ9D+WY79C8ZliACw== + dependencies: + "@octokit/auth-token" "^2.4.0" + "@octokit/graphql" "^4.3.1" + "@octokit/request" "^5.4.0" + "@octokit/types" "^5.0.0" + before-after-hook "^2.1.0" + universal-user-agent "^5.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.3.tgz#dd09b599662d7e1b66374a177ab620d8cdf73487" + integrity sha512-Y900+r0gIz+cWp6ytnkibbD95ucEzDSKzlEnaWS52hbCDNcCJYO5mRmWW7HRAnDc7am+N/5Lnd8MppSaTYx1Yg== + dependencies: + "@octokit/types" "^5.0.0" + is-plain-object "^3.0.0" + universal-user-agent "^5.0.0" + +"@octokit/graphql@^4.3.1": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.1.tgz#162aed1490320b88ce34775b3f6b8de945529fa9" + integrity sha512-qgMsROG9K2KxDs12CO3bySJaYoUu2aic90qpFrv7A8sEBzZ7UFGvdgPKiLw5gOPYEYbS0Xf8Tvf84tJutHPulQ== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/types" "^5.0.0" + universal-user-agent "^5.0.0" + +"@octokit/plugin-paginate-rest@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.3.tgz#a6ad4377e7e7832fb4bdd9d421e600cb7640ac27" + integrity sha512-eKTs91wXnJH8Yicwa30jz6DF50kAh7vkcqCQ9D7/tvBAP5KKkg6I2nNof8Mp/65G0Arjsb4QcOJcIEQY+rK1Rg== + dependencies: + "@octokit/types" "^5.0.0" + +"@octokit/plugin-rest-endpoint-methods@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.0.0.tgz#b02a2006dda8e908c3f8ab381dd5475ef5a810a8" + integrity sha512-emS6gysz4E9BNi9IrCl7Pm4kR+Az3MmVB0/DoDCmF4U48NbYG3weKyDlgkrz6Jbl4Mu4nDx8YWZwC4HjoTdcCA== + dependencies: + "@octokit/types" "^5.0.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.2.tgz#0e76b83f5d8fdda1db99027ea5f617c2e6ba9ed0" + integrity sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw== + dependencies: + "@octokit/types" "^5.0.1" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.3.0", "@octokit/request@^5.4.0": + version "5.4.5" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.5.tgz#8df65bd812047521f7e9db6ff118c06ba84ac10b" + integrity sha512-atAs5GAGbZedvJXXdjtKljin+e2SltEs48B3naJjqWupYl2IUBbB/CJisyjbNHcKpHzb3E+OYEZ46G8eakXgQg== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.0.0" + "@octokit/types" "^5.0.0" + deprecation "^2.0.0" + is-plain-object "^3.0.0" + node-fetch "^2.3.0" + once "^1.4.0" + universal-user-agent "^5.0.0" + +"@octokit/types@^5.0.0", "@octokit/types@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-5.0.1.tgz#5459e9a5e9df8565dcc62c17a34491904d71971e" + integrity sha512-GorvORVwp244fGKEt3cgt/P+M0MGy4xEDbckw+K5ojEezxyMDgCaYPKVct+/eWQfZXOT7uq0xRpmrl/+hliabA== + dependencies: + "@types/node" ">= 8" + "@types/babel__core@^7.1.0": version "7.1.9" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d" @@ -569,7 +668,7 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/node@*": +"@types/node@*", "@types/node@>= 8": version "14.0.19" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.19.tgz#994d99708822bca643a2364f8aeed04a16e0f5a1" integrity sha512-yf3BP/NIXF37BjrK5klu//asUWitOEoUP5xE1mhSUjazotwJ/eJDgEmMQNlOeWOVv72j24QQ+3bqXHE++CFGag== @@ -983,6 +1082,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +before-after-hook@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" + integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -1368,6 +1472,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + detect-newline@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" @@ -2515,6 +2624,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-plain-object@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" + integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== + is-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" @@ -3277,6 +3391,11 @@ lower-case@^2.0.1: dependencies: tslib "^1.10.0" +macos-release@^2.2.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.0.tgz#837b39fc01785c3584f103c5599e0f0c8068b49e" + integrity sha512-ko6deozZYiAkqa/0gmcsz+p4jSy3gY7/ZsCEokPaYd8k+6/aXGkiTgr61+Owup7Sf+xjqW8u2ElhoM9SEcEfuA== + make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -3455,7 +3574,7 @@ no-case@^3.0.3: lower-case "^2.0.1" tslib "^1.10.0" -node-fetch@2.6.0: +node-fetch@2.6.0, node-fetch@^2.3.0: version "2.6.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== @@ -3640,6 +3759,14 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" word-wrap "~1.2.3" +os-name@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" + integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg== + dependencies: + macos-release "^2.2.0" + windows-release "^3.1.0" + os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -4735,6 +4862,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -4779,6 +4911,13 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +universal-user-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9" + integrity sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q== + dependencies: + os-name "^3.1.0" + universalify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" @@ -4932,6 +5071,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +windows-release@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.1.tgz#cb4e80385f8550f709727287bf71035e209c4ace" + integrity sha512-Pngk/RDCaI/DkuHPlGTdIkDiTAnAkyMjoQMZqRsxydNl1qGXNIoZrB7RK8g53F2tEgQBMqQJHQdYZuQEEAu54A== + dependencies: + execa "^1.0.0" + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"