From d8f8b0c5d3564e8d3dccef5603581686add49bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A1=83=E6=A0=91=E5=A4=AD?= Date: Thu, 19 Jan 2023 15:28:27 +0800 Subject: [PATCH] update ci --- .github/workflows/ci.yml | 128 +++++++++++++++++++++++++++++ .github/workflows/release.yml | 90 --------------------- .github/workflows/updater.yml | 36 +++++++++ UPDATE_LOG.md | 2 +- package.json | 9 +-- scripts/aarch.mjs | 98 +++++++++++++++++++++++ scripts/publish.mjs | 53 ++++++++++++ scripts/updatelog.mjs | 44 ++++++++++ scripts/updater.mjs | 146 ++++++++++++++++++++++++++++++++++ src-tauri/tauri.conf.json | 13 +-- yarn.lock | 11 +-- 11 files changed, 515 insertions(+), 115 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/updater.yml create mode 100644 scripts/aarch.mjs create mode 100644 scripts/publish.mjs create mode 100644 scripts/updatelog.mjs create mode 100644 scripts/updater.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..04d05cd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,128 @@ +name: Release CI + +on: + workflow_dispatch: + push: + tags: + - v** + +env: + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: short + +jobs: + release: + strategy: + matrix: + # os: [windows-latest, ubuntu-latest, macos-latest] + os: [macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Install Node + uses: actions/setup-node@v1 + with: + node-version: 16 + + - name: Install Dependencies (ubuntu only) + if: startsWith(matrix.os, 'ubuntu-') + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf + + - name: Reconfigure git to use HTTP authentication + run: > + git config --global url."https://github.com/".insteadOf + ssh://git@github.com/ + + - name: Get yarn cache dir path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Yarn Cache + uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Yarn install and check + run: | + yarn install --network-timeout 1000000 + yarn run check + + - name: Tauri build + uses: tauri-apps/tauri-action@v0 + # enable cache even though failed + # continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + with: + tagName: v__VERSION__ + releaseName: "Lanaya v__VERSION__" + releaseBody: "More new features are now supported." + releaseDraft: false + prerelease: true + + # - name: Portable Bundle + # if: matrix.os == 'windows-latest' + # # rebuild with env settings + # run: | + # yarn build + # yarn run portable + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + # TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + # VITE_WIN_PORTABLE: 1 + + release-update: + needs: release + runs-on: macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Reconfigure git to use HTTP authentication + run: > + git config --global url."https://github.com/".insteadOf + ssh://git@github.com/ + + - name: Get yarn cache dir path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Yarn Cache + uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Yarn install + run: yarn install + + - name: Release updater file + run: yarn run updater + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index c368c65..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Release CI - -on: - push: - # Sequence of patterns matched against refs/tags - tags: - - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 - -jobs: - create-release: - runs-on: ubuntu-20.04 - outputs: - RELEASE_UPLOAD_ID: ${{ steps.create_release.outputs.id }} - - steps: - - uses: actions/checkout@v2 - - name: Query version number - id: get_version - shell: bash - run: | - echo "using version tag ${GITHUB_REF:10}" - echo "version=${GITHUB_REF:10}" >> $GITHUB_ENV - - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: "${{ env.version }}" - release_name: "Lanaya ${{ env.version }}" - body: "See the assets to download this version and install." - - build-tauri: - needs: create-release - strategy: - fail-fast: false - matrix: - platform: [macos-latest] - # platform: [macos-latest, ubuntu-latest, windows-latest] - - runs-on: ${{ matrix.platform }} - steps: - - uses: actions/checkout@v3 - - name: setup node - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: install Rust stable - uses: dtolnay/rust-toolchain@stable - - name: install dependencies (ubuntu only) - if: matrix.platform == 'ubuntu-20.04' - run: | - sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf - - - name: Reconfigure git to use HTTP authentication - run: > - git config --global url."https://github.com/".insteadOf - ssh://git@github.com/ - - name: Install app dependencies and build it - run: yarn && yarn build - - - uses: tauri-apps/tauri-action@v0.3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} - TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} - with: - releaseId: ${{ needs.create-release.outputs.RELEASE_UPLOAD_ID }} - - # 生成静态资源并将其推送到 github pages - updater: - runs-on: ubuntu-20.04 - needs: [create-release, build-tauri] - - steps: - - uses: actions/checkout@v2 - - name: Reconfigure git to use HTTP authentication - run: > - git config --global url."https://github.com/".insteadOf - ssh://git@github.com/ - - run: yarn && yarn updater --token=${{ secrets.GITHUB_TOKEN }} - - name: Deploy install.json - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./updater - force_orphan: true diff --git a/.github/workflows/updater.yml b/.github/workflows/updater.yml new file mode 100644 index 0000000..1d881ad --- /dev/null +++ b/.github/workflows/updater.yml @@ -0,0 +1,36 @@ +name: Updater CI + +on: workflow_dispatch + +jobs: + release-update: + runs-on: macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Reconfigure git to use HTTP authentication + run: > + git config --global url."https://github.com/".insteadOf + ssh://git@github.com/ + + - name: Get yarn cache dir path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Yarn Cache + uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Yarn install + run: yarn install + + - name: Release updater file + run: yarn run updater + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/UPDATE_LOG.md b/UPDATE_LOG.md index 52899ba..50406e2 100644 --- a/UPDATE_LOG.md +++ b/UPDATE_LOG.md @@ -2,7 +2,7 @@ All changes will be documented in this file. -## v0.0.7 +## v0.0.8 ### Feature diff --git a/package.json b/package.json index 066b973..60fb90f 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { "name": "lanaya", "private": true, - "version": "0.0.0", + "version": "0.0.7", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", - "updater": "tr updater", - "release": "tr release --git", - "tr": "tr", + "aarch": "node scripts/aarch.mjs", + "updater": "node scripts/updater.mjs", + "publish": "node scripts/publish.mjs", "tauri": "tauri" }, "dependencies": { @@ -27,7 +27,6 @@ "devDependencies": { "@actions/github": "^5.1.1", "@tauri-apps/cli": "^1.2.3", - "@tauri-release/cli": "^0.2.3", "@vitejs/plugin-vue": "^3.2.0", "autoprefixer": "^10.4.13", "node-fetch": "^3.3.0", diff --git a/scripts/aarch.mjs b/scripts/aarch.mjs new file mode 100644 index 0000000..5591e92 --- /dev/null +++ b/scripts/aarch.mjs @@ -0,0 +1,98 @@ +/** + * Build and upload assets for macOS(aarch) + */ +import fs from "fs-extra"; +import path from "path"; +import { exit } from "process"; +import { createRequire } from "module"; +import { getOctokit, context } from "@actions/github"; + +const require = createRequire(import.meta.url); + +async function resolve() { + if (!process.env.GITHUB_TOKEN) { + throw new Error("GITHUB_TOKEN is required"); + } + if (!process.env.GITHUB_REPOSITORY) { + throw new Error("GITHUB_REPOSITORY is required"); + } + if (!process.env.TAURI_PRIVATE_KEY) { + throw new Error("TAURI_PRIVATE_KEY is required"); + } + if (!process.env.TAURI_KEY_PASSWORD) { + throw new Error("TAURI_KEY_PASSWORD is required"); + } + + const { version } = require("../package.json"); + + const cwd = process.cwd(); + const bundlePath = path.join(cwd, "src-tauri/target/release/bundle"); + const join = (p) => path.join(bundlePath, p); + + const appPathList = [join("macos/Lanaya.app.tar.gz"), join("macos/Lanaya.app.tar.gz.sig")]; + + for (const appPath of appPathList) { + if (fs.pathExistsSync(appPath)) { + fs.removeSync(appPath); + } + } + + fs.copyFileSync(join("macos/Lanaya.app.tar.gz"), appPathList[0]); + fs.copyFileSync(join("macos/Lanaya.app.tar.gz.sig"), appPathList[1]); + + const options = { owner: context.repo.owner, repo: context.repo.repo }; + const github = getOctokit(process.env.GITHUB_TOKEN); + + const { data: release } = await github.rest.repos.getReleaseByTag({ + ...options, + tag: `v${version}`, + }); + + if (!release.id) throw new Error("failed to find the release"); + + await uploadAssets(release.id, [join(`dmg/Lanaya_${version}_aarch64.dmg`), ...appPathList]); +} + +// From tauri-apps/tauri-action +// https://github.com/tauri-apps/tauri-action/blob/dev/packages/action/src/upload-release-assets.ts +async function uploadAssets(releaseId, assets) { + const github = getOctokit(process.env.GITHUB_TOKEN); + + // Determine content-length for header to upload asset + const contentLength = (filePath) => fs.statSync(filePath).size; + + for (const assetPath of assets) { + const headers = { + "content-type": "application/zip", + "content-length": contentLength(assetPath), + }; + + const ext = path.extname(assetPath); + const filename = path.basename(assetPath).replace(ext, ""); + const assetName = path.dirname(assetPath).includes(`target${path.sep}debug`) + ? `${filename}-debug${ext}` + : `${filename}${ext}`; + + console.log(`[INFO]: Uploading ${assetName}...`); + + try { + await github.rest.repos.uploadReleaseAsset({ + headers, + name: assetName, + data: fs.readFileSync(assetPath), + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId, + }); + } catch (error) { + console.log(error.message); + } + } +} + +if (process.platform === "darwin" && process.arch === "arm64") { + resolve(); +} else { + console.error("invalid"); + exit(1); +} diff --git a/scripts/publish.mjs b/scripts/publish.mjs new file mode 100644 index 0000000..523ad18 --- /dev/null +++ b/scripts/publish.mjs @@ -0,0 +1,53 @@ +import fs from "fs-extra"; +import { createRequire } from "module"; +import { execSync } from "child_process"; +import { resolveUpdateLog } from "./updatelog.mjs"; + +const require = createRequire(import.meta.url); + +// publish +async function resolvePublish() { + const flag = process.argv[2] ?? "patch"; + const packageJson = require("../package.json"); + const tauriJson = require("../src-tauri/tauri.conf.json"); + + let [a, b, c] = packageJson.version.split(".").map(Number); + + if (flag === "major") { + a += 1; + b = 0; + c = 0; + } else if (flag === "minor") { + b += 1; + c = 0; + } else if (flag === "patch") { + c += 1; + } else throw new Error(`invalid flag "${flag}"`); + + const nextVersion = `${a}.${b}.${c}`; + packageJson.version = nextVersion; + tauriJson.package.version = nextVersion; + + // 发布更新前先写更新日志 + const nextTag = `v${nextVersion}`; + await resolveUpdateLog(nextTag); + + await fs.writeFile( + "./package.json", + JSON.stringify(packageJson, undefined, 2) + ); + await fs.writeFile( + "./src-tauri/tauri.conf.json", + JSON.stringify(tauriJson, undefined, 2) + ); + + execSync("git add ./package.json"); + execSync("git add ./src-tauri/tauri.conf.json"); + execSync(`git commit -m "v${nextVersion}"`); + execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`); + execSync(`git push`); + execSync(`git push origin v${nextVersion}`); + console.log(`Publish Successfully...`); +} + +resolvePublish(); diff --git a/scripts/updatelog.mjs b/scripts/updatelog.mjs new file mode 100644 index 0000000..795e9e5 --- /dev/null +++ b/scripts/updatelog.mjs @@ -0,0 +1,44 @@ +import fs from "fs-extra"; +import path from "path"; + +const UPDATE_LOG = "UPDATE_LOG.md"; + +// parse the UPDATELOG.md +export async function resolveUpdateLog(tag) { + const cwd = process.cwd(); + + const reTitle = /^## v[\d\.]+/; + const reEnd = /^---/; + + const file = path.join(cwd, UPDATE_LOG); + + if (!(await fs.pathExists(file))) { + throw new Error("could not found UPDATELOG.md"); + } + + const data = await fs.readFile(file).then((d) => d.toString("utf8")); + + const map = {}; + let p = ""; + + data.split("\n").forEach((line) => { + if (reTitle.test(line)) { + p = line.slice(3).trim(); + if (!map[p]) { + map[p] = []; + } else { + throw new Error(`Tag ${p} dup`); + } + } else if (reEnd.test(line)) { + p = ""; + } else if (p) { + map[p].push(line); + } + }); + + if (!map[tag]) { + throw new Error(`could not found "${tag}" in UPDATELOG.md`); + } + + return map[tag].join("\n").trim(); +} diff --git a/scripts/updater.mjs b/scripts/updater.mjs new file mode 100644 index 0000000..d990589 --- /dev/null +++ b/scripts/updater.mjs @@ -0,0 +1,146 @@ +import fetch from "node-fetch"; +import { getOctokit, context } from "@actions/github"; +import { resolveUpdateLog } from "./updatelog.mjs"; + +const UPDATE_TAG_NAME = "updater"; +const UPDATE_JSON_FILE = "update.json"; + +/// generate update.json +/// upload to update tag's release asset +async function resolveUpdater() { + if (process.env.GITHUB_TOKEN === undefined) { + throw new Error("GITHUB_TOKEN is required"); + } + + const options = { owner: context.repo.owner, repo: context.repo.repo }; + const github = getOctokit(process.env.GITHUB_TOKEN); + + const { data: tags } = await github.rest.repos.listTags({ + ...options, + per_page: 10, + page: 1, + }); + + // get the latest publish tag + const tag = tags.find((t) => t.name.startsWith("v")); + + console.log(tag); + + const { data: latestRelease } = await github.rest.repos.getReleaseByTag({ + ...options, + tag: tag.name, + }); + + const updateData = { + name: tag.name, + notes: await resolveUpdateLog(tag.name), // use updatelog.md + pub_date: new Date().toISOString(), + platforms: { + // win64: { signature: "", url: "" }, // compatible with older formats + // linux: { signature: "", url: "" }, // compatible with older formats + // darwin: { signature: "", url: "" }, // compatible with older formats + "darwin-aarch64": { signature: "", url: "" }, + "darwin-intel": { signature: "", url: "" }, + // "linux-x86_64": { signature: "", url: "" }, + // "windows-x86_64": { signature: "", url: "" }, + }, + }; + + const promises = latestRelease.assets.map(async (asset) => { + const { name, browser_download_url } = asset; + + // // win64 url + // if (name.endsWith(".msi.zip") && name.includes("en-US")) { + // updateData.platforms.win64.url = browser_download_url; + // updateData.platforms["windows-x86_64"].url = browser_download_url; + // } + // // win64 signature + // if (name.endsWith(".msi.zip.sig") && name.includes("en-US")) { + // const sig = await getSignature(browser_download_url); + // updateData.platforms.win64.signature = sig; + // updateData.platforms["windows-x86_64"].signature = sig; + // } + + // darwin url (intel) + if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) { + // updateData.platforms.darwin.url = browser_download_url; + updateData.platforms["darwin-intel"].url = browser_download_url; + } + // darwin signature (intel) + if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) { + const sig = await getSignature(browser_download_url); + // updateData.platforms.darwin.signature = sig; + updateData.platforms["darwin-intel"].signature = sig; + } + + // darwin url (aarch) + if (name.endsWith("aarch64.app.tar.gz")) { + updateData.platforms["darwin-aarch64"].url = browser_download_url; + } + // darwin signature (aarch) + if (name.endsWith("aarch64.app.tar.gz.sig")) { + const sig = await getSignature(browser_download_url); + updateData.platforms["darwin-aarch64"].signature = sig; + } + + // // linux url + // if (name.endsWith(".AppImage.tar.gz")) { + // updateData.platforms.linux.url = browser_download_url; + // updateData.platforms["linux-x86_64"].url = browser_download_url; + // } + // // linux signature + // if (name.endsWith(".AppImage.tar.gz.sig")) { + // const sig = await getSignature(browser_download_url); + // updateData.platforms.linux.signature = sig; + // updateData.platforms["linux-x86_64"].signature = sig; + // } + }); + + await Promise.allSettled(promises); + console.log(updateData); + + // maybe should test the signature as well + // delete the null field + Object.entries(updateData.platforms).forEach(([key, value]) => { + if (!value.url) { + console.log(`[Error]: failed to parse release for "${key}"`); + delete updateData.platforms[key]; + } + }); + + // update the update.json + const { data: updateRelease } = await github.rest.repos.getReleaseByTag({ + ...options, + tag: UPDATE_TAG_NAME, + }); + + // delete the old assets + for (let asset of updateRelease.assets) { + if (asset.name === UPDATE_JSON_FILE) { + await github.rest.repos.deleteReleaseAsset({ + ...options, + asset_id: asset.id, + }); + } + } + + // upload new assets + await github.rest.repos.uploadReleaseAsset({ + ...options, + release_id: updateRelease.id, + name: UPDATE_JSON_FILE, + data: JSON.stringify(updateData, null, 2), + }); +} + +// get the signature file content +async function getSignature(url) { + const response = await fetch(url, { + method: "GET", + headers: { "Content-Type": "application/octet-stream" }, + }); + + return response.text(); +} + +resolveUpdater().catch(console.error); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 551e31a..d642dd4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -34,6 +34,7 @@ }, "bundle": { "active": true, + "targets": "all", "category": "DeveloperTool", "copyright": "© 2023 churchTao All Rights Reserved", "deb": { @@ -58,16 +59,12 @@ }, "resources": [], "shortDescription": "Clipboard manager", - "targets": "all", "windows": { "certificateThumbprint": null, "digestAlgorithm": "sha256", "timestampUrl": "", "wix": { - "language": [ - "zh-CN", - "en-US" - ] + "language": ["zh-CN", "en-US"] } } }, @@ -77,9 +74,7 @@ "updater": { "active": true, "dialog": true, - "endpoints": [ - "https://releases.myapp.com/{{target}}/{{current_version}}" - ], + "endpoints": ["https://lencx.github.io/ChatGPT/install.json"], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFFQkYyRTBBMzk3MkM0QTcKUldTbnhISTVDaTYvSHJXbFpZYzNydW8yU0lEN2JjOFFYTHNpZFZnMmxRTWM1SUtjM0ZlcThlaVkK" }, "windows": [ @@ -96,4 +91,4 @@ } ] } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 64f345f..d2da597 100644 --- a/yarn.lock +++ b/yarn.lock @@ -304,15 +304,6 @@ "@tauri-apps/cli-win32-ia32-msvc" "1.2.3" "@tauri-apps/cli-win32-x64-msvc" "1.2.3" -"@tauri-release/cli@^0.2.3": - version "0.2.3" - resolved "https://mirrors.cloud.tencent.com/npm/@tauri-release%2fcli/-/cli-0.2.3.tgz#71ed2a9bbe7900e1796d5c8e995b2299a82d34da" - integrity sha512-Xzn/UtOpu68Ic21GmhTMKWqBXo1Al6TfUkuBy7EMuy6p2LPoLGHT3rND1yjqbqco0drU8wsAU8xGahFuvXLY6A== - dependencies: - "@actions/github" "^5.1.1" - lodash "^4.17.21" - node-fetch "^3.3.0" - "@vitejs/plugin-vue@^3.2.0": version "3.2.0" resolved "https://mirrors.cloud.tencent.com/npm/@vitejs%2fplugin-vue/-/plugin-vue-3.2.0.tgz" @@ -904,7 +895,7 @@ lilconfig@^2.0.5, lilconfig@^2.0.6: resolved "https://mirrors.cloud.tencent.com/npm/lilconfig/-/lilconfig-2.0.6.tgz" integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== -lodash@^4.17.20, lodash@^4.17.21: +lodash@^4.17.20: version "4.17.21" resolved "https://mirrors.cloud.tencent.com/npm/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==