From 728892298df3ff5f012e83038d86f086b454ff26 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Mon, 31 Aug 2026 21:55:35 +0800 Subject: [PATCH 1/2] chore(automation): simplify repository delivery boundaries - align contract admission and Dependabot with intentional adoption - remove redundant CI and Pages authorities - move native extension publishing into a repository script --- .github/dependabot.yml | 4 + .github/workflows/ci.yml | 91 ----------------------- .github/workflows/extension-release.yml | 77 +------------------ .github/workflows/pages-deploy.yml | 15 ---- .github/workflows/pages-preview.yml | 79 -------------------- AGENTS.md | 2 +- docs/40-deployment/development-runtime.md | 11 ++- scripts/check-local-runtime-contract.mjs | 19 ++--- scripts/publish-native-extensions.sh | 86 +++++++++++++++++++++ 9 files changed, 109 insertions(+), 275 deletions(-) create mode 100644 scripts/publish-native-extensions.sh diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 55088104..a17ae964 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,6 +21,10 @@ updates: semver-major-days: 30 semver-minor-days: 7 semver-patch-days: 3 + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major groups: production-dependencies: dependency-type: production diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd5febc4..2f663dc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,9 +12,6 @@ on: - synchronize - reopened - ready_for_review - merge_group: - types: - - checks_requested workflow_dispatch: permissions: @@ -25,7 +22,6 @@ concurrency: group: >- client-checks-${{ github.event.pull_request.number || - github.event.merge_group.head_sha || github.ref }} cancel-in-progress: true @@ -91,60 +87,6 @@ jobs: echo "- Runtime contract: \`$CONTRACT_REVISION\`" } >> "$GITHUB_STEP_SUMMARY" - database-contract: - name: Database contract - needs: core-release - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - - name: Install the pinned pnpm release - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - version: 11.11.0 - - - name: Set up Node - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 - with: - node-version-file: package.json - cache: pnpm - registry-url: https://npm.pkg.github.com - scope: '@inkcre' - - - name: Install the frozen workspace - run: pnpm install --frozen-lockfile - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Authenticate to GHCR - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate types from the selected raw schema - run: pnpm contract:sync -- --image "${{ needs.core-release.outputs.image }}" - - - name: Upload the generated database contract - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: generated-database-contract-${{ github.sha }} - path: | - packages/core/src/database/database.generated.ts - packages/core/src/database/runtime-contract.generated.json - if-no-files-found: error - retention-days: 7 - - - name: Reject a stale checked-in database contract - run: >- - git diff --exit-code -- - packages/core/src/database/database.generated.ts - packages/core/src/database/runtime-contract.generated.json - workspace: name: Workspace contract runs-on: ubuntu-latest @@ -182,39 +124,6 @@ jobs: --core-package packages/core/package.json --artifact-directory extensions/twitter/dist/client-web - toolchain-shadow: - name: Type-aware and TypeScript 7 shadow - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Install the pinned pnpm release - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - with: - version: 11.11.0 - - - name: Set up Node - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 - with: - node-version-file: package.json - cache: pnpm - registry-url: https://npm.pkg.github.com - scope: '@inkcre' - - - name: Install the frozen workspace - run: pnpm install --frozen-lockfile - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Run type-aware Oxlint - continue-on-error: true - run: pnpm lint:type-aware - - - name: Run the native TypeScript 7 preview - continue-on-error: true - run: pnpm type-check:ts7 - peer-database: name: client-web E2E needs: core-release diff --git a/.github/workflows/extension-release.yml b/.github/workflows/extension-release.yml index 11eb8ec0..9448ada5 100644 --- a/.github/workflows/extension-release.yml +++ b/.github/workflows/extension-release.yml @@ -107,79 +107,4 @@ jobs: INKCRE_EXTENSION_REGISTRY_TOKEN: ${{ secrets.INKCRE_EXTENSION_REGISTRY_TOKEN }} SOURCE_REPOSITORY: https://github.com/${{ github.repository }} SOURCE_REVISION: ${{ github.sha }} - run: | - test -n "$INKCRE_EXTENSION_REGISTRY_TOKEN" - mkdir -p .extension-delivery - - for package_path in extensions/*/package.json; do - descriptor="$(node --input-type=module --eval ' - import { readFile } from "node:fs/promises" - import path from "node:path" - const packagePath = process.argv[1] - const manifest = JSON.parse(await readFile(packagePath, "utf8")) - if (!manifest.inkcre?.module_federation) process.exit(0) - process.stdout.write([path.basename(path.dirname(packagePath)), manifest.inkcre.name, manifest.version].join("\t")) - ' "$package_path")" - if [[ -z "$descriptor" ]]; then - continue - fi - IFS=$'\t' read -r extension_directory extension_path release_path <<< "$descriptor" - - delivery_directory=".extension-delivery/$extension_directory" - artifact_directory="extensions/$extension_directory/dist/client-web" - mkdir -p "$delivery_directory" - public_descriptor="$delivery_directory/public-release.json" - status="$(curl --silent --show-error --output "$public_descriptor" --write-out '%{http_code}' \ - "${INKCRE_EXTENSION_REGISTRY_URL%/}/v1/extensions/${extension_path}/releases/${release_path}")" - if [[ "$status" == '200' ]]; then - has_native_association="$(node --input-type=module --eval 'import { readFile } from "node:fs/promises"; const release = JSON.parse(await readFile(process.argv[1], "utf8")); process.stdout.write(String(release.module_federation !== null))' "$public_descriptor")" - elif [[ "$status" == '404' ]]; then - has_native_association=false - else - echo "Cannot determine $extension_path $release_path Registry state: HTTP $status." >&2 - exit 1 - fi - if [[ "$has_native_association" == 'true' ]]; then - echo "$extension_path $release_path is already published." >> "$GITHUB_STEP_SUMMARY" - continue - fi - - node scripts/verify-native-extension-distribution.mjs prepare-body \ - --package "$package_path" \ - --core-package packages/core/package.json \ - --artifact-directory "$artifact_directory" \ - --source-repository "$SOURCE_REPOSITORY" \ - --source-revision "$SOURCE_REVISION" \ - --build-id "client-web-extension-release-${GITHUB_RUN_ID}" \ - --output "$delivery_directory/prepare.json" - ( - cd "$artifact_directory" - zip -q -r "$GITHUB_WORKSPACE/$delivery_directory/snapshot.zip" . - ) - - curl --fail-with-body --silent --show-error \ - --request POST \ - --header "Authorization: Bearer $INKCRE_EXTENSION_REGISTRY_TOKEN" \ - --header 'Content-Type: application/json' \ - --data-binary "@$delivery_directory/prepare.json" \ - "${INKCRE_EXTENSION_REGISTRY_URL%/}/v1/extensions/${extension_path}/releases" \ - > "$delivery_directory/prepared-release.json" - curl --fail-with-body --silent --show-error \ - --request POST \ - --header "Authorization: Bearer $INKCRE_EXTENSION_REGISTRY_TOKEN" \ - --form "content=@$delivery_directory/snapshot.zip;type=application/zip" \ - "${INKCRE_EXTENSION_REGISTRY_URL%/}/v1/extensions/${extension_path}/releases/${release_path}/module-federation" \ - > "$delivery_directory/uploaded-release.json" - curl --fail-with-body --silent --show-error \ - --request POST \ - --header "Authorization: Bearer $INKCRE_EXTENSION_REGISTRY_TOKEN" \ - "${INKCRE_EXTENSION_REGISTRY_URL%/}/v1/extensions/${extension_path}/releases/${release_path}/publish" \ - > "$delivery_directory/published-release.json" - node scripts/verify-native-extension-distribution.mjs verify-public \ - --registry-url "$INKCRE_EXTENSION_REGISTRY_URL" \ - --package "$package_path" \ - --core-package packages/core/package.json \ - --artifact-directory "$artifact_directory" \ - --output "$delivery_directory/verification.json" - echo "$extension_path $release_path published and verified." >> "$GITHUB_STEP_SUMMARY" - done + run: bash scripts/publish-native-extensions.sh diff --git a/.github/workflows/pages-deploy.yml b/.github/workflows/pages-deploy.yml index 458e5596..7b166145 100644 --- a/.github/workflows/pages-deploy.yml +++ b/.github/workflows/pages-deploy.yml @@ -89,21 +89,6 @@ jobs: working-directory: release run: pnpm build - - name: Reverify main before Pages delivery - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - SOURCE_SHA: ${{ needs.identity.outputs.source_sha }} - with: - script: | - const currentMain = await github.rest.git.getRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: 'heads/main', - }) - if (currentMain.data.object.sha !== process.env.SOURCE_SHA) { - core.setFailed('Main changed after the release build; refusing Pages delivery.') - } - - name: Deploy to Cloudflare Pages id: pages uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 diff --git a/.github/workflows/pages-preview.yml b/.github/workflows/pages-preview.yml index 0bec7c5f..46450ac6 100644 --- a/.github/workflows/pages-preview.yml +++ b/.github/workflows/pages-preview.yml @@ -12,7 +12,6 @@ on: permissions: contents: read packages: read - pull-requests: read jobs: identity: @@ -57,7 +56,6 @@ jobs: contents: read deployments: write packages: read - pull-requests: read runs-on: ubuntu-latest timeout-minutes: 25 environment: preview @@ -123,61 +121,6 @@ jobs: cp -R .preview-registry/. apps/client-web/dist/ cp -R apps/client-web/dist "$GITHUB_WORKSPACE/.pages-preview" - - name: Reverify pull-request identity before delivery - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - PREVIEW_BRANCH: ${{ needs.identity.outputs.branch }} - PREVIEW_HEAD_SHA: ${{ needs.identity.outputs.head_sha }} - PULL_NUMBER: ${{ needs.identity.outputs.pull_number }} - with: - script: | - const expectedBranch = `preview/client-web/pr-${process.env.PULL_NUMBER}` - if (process.env.PREVIEW_BRANCH !== expectedBranch) { - core.setFailed(`Unexpected Pages preview branch: ${process.env.PREVIEW_BRANCH}`) - return - } - const { data: pull } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: Number(process.env.PULL_NUMBER), - }) - if ( - pull.state !== 'open' || - pull.base.ref !== 'main' || - pull.head.sha !== process.env.PREVIEW_HEAD_SHA || - pull.head.repo?.full_name !== `${context.repo.owner}/${context.repo.repo}` - ) { - core.setFailed('Pull-request identity changed after the exact-head build.') - } - - - name: Register the pull-request deployment - id: github-deployment - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - PREVIEW_HEAD_SHA: ${{ needs.identity.outputs.head_sha }} - PULL_NUMBER: ${{ needs.identity.outputs.pull_number }} - with: - script: | - const { data: deployment } = await github.rest.repos.createDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: process.env.PREVIEW_HEAD_SHA, - environment: 'preview', - description: `Cloudflare Pages preview for PR #${process.env.PULL_NUMBER}`, - auto_merge: false, - required_contexts: [], - transient_environment: true, - production_environment: false, - }) - core.setOutput('deployment_id', String(deployment.id)) - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id, - state: 'in_progress', - log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, - }) - - name: Deploy to the isolated Pages preview branch id: pages uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 @@ -193,25 +136,3 @@ jobs: --branch=${{ needs.identity.outputs.branch }} --commit-hash=${{ needs.identity.outputs.head_sha }} --commit-dirty=false - - - name: Report the pull-request deployment - if: ${{ always() && steps.github-deployment.outputs.deployment_id != '' }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - DEPLOYMENT_ID: ${{ steps.github-deployment.outputs.deployment_id }} - JOB_STATUS: ${{ job.status }} - PREVIEW_URL: ${{ steps.pages.outputs.pages-deployment-alias-url }} - with: - script: | - const success = process.env.JOB_STATUS === 'success' - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: Number(process.env.DEPLOYMENT_ID), - state: success ? 'success' : 'failure', - description: success ? 'Pull-request preview is ready' : 'Pull-request preview failed', - log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, - ...(success && process.env.PREVIEW_URL - ? { environment_url: process.env.PREVIEW_URL } - : {}), - }) diff --git a/AGENTS.md b/AGENTS.md index eb488c9d..ff0afdb3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,7 @@ Reason in English. Communicate with humans in Chinese. - Diagnose: `pnpm run doctor` (`pnpm doctor` is a different pnpm command). - Develop: `pnpm dev`, `pnpm dev:all`, or an explicit source lane documented in `docs/40-deployment/development-runtime.md`. - Required verification: `pnpm check`; required production outputs: `pnpm build`. -- Shadow verification: `pnpm lint:type-aware` and `pnpm type-check:ts7`. +- Optional local migration probes: `pnpm lint:type-aware` and `pnpm type-check:ts7`. - Follow the organization-wide [Verification and Test Policy](https://github.com/InKCre/.github/blob/main/TESTING.md). The admitted Playwright E2E suites do not authorize new automation by analogy. - Follow `.agents/prompts/code-for-human.md` for source changes. diff --git a/docs/40-deployment/development-runtime.md b/docs/40-deployment/development-runtime.md index 7afcedbd..3bd3bf1d 100644 --- a/docs/40-deployment/development-runtime.md +++ b/docs/40-deployment/development-runtime.md @@ -88,10 +88,13 @@ accepts only a ready development runtime, and resets only this worktree's client resources. Client-web refuses to reset or stop an external core-py runtime; recovery and cleanup must go through core-py. Browser E2E always creates and removes its own isolated database runtime. -The database contract is generated from the selected immutable core release. CI regenerates and -checks it, runs the real core service in an isolated browser/database chain, and rejects a moved -`stable` selection before admission. Pull-request evidence does not establish source-branch or -migration equality between repositories. +The checked database projection records the contract revision intentionally adopted by this +consumer. Update it with `pnpm contract:sync` when client work adopts a newer compatible revision; +an unrelated pull request does not chase a moving `stable` channel. CI still resolves `stable` once +to an immutable image and runs that real core service through the isolated browser/database chain, +which proves the checked client remains compatible with current delivery. A breaking contract +revision requires the coordinated producer-first migration defined by the shared contract. +Pull-request evidence does not establish source-branch or migration equality between repositories. ## Operator Commands diff --git a/scripts/check-local-runtime-contract.mjs b/scripts/check-local-runtime-contract.mjs index e682d38d..6617460b 100644 --- a/scripts/check-local-runtime-contract.mjs +++ b/scripts/check-local-runtime-contract.mjs @@ -50,20 +50,21 @@ if ( if (await exists('.node-version')) { errors.push('.node-version must remain absent; pnpm devEngines.runtime owns Node') } -for (const [path, expectedNodeFiles] of [ - ['.github/workflows/ci.yml', Array(6).fill('package.json')], - ['.github/workflows/pages-cleanup.yml', ['controller/package.json']], - ['.github/workflows/pages-preview.yml', ['candidate/package.json']], - ['.github/workflows/pages-deploy.yml', ['release/package.json']], +for (const [path, expectedNodeFile] of [ + ['.github/workflows/ci.yml', 'package.json'], + ['.github/workflows/pages-cleanup.yml', 'controller/package.json'], + ['.github/workflows/pages-preview.yml', 'candidate/package.json'], + ['.github/workflows/pages-deploy.yml', 'release/package.json'], ]) { const workflow = await readFile(`${repoRoot}/${path}`, 'utf8') const configuredNodeFiles = [...workflow.matchAll(/node-version-file:\s*(\S+)/g)].map( (match) => match[1] ) - if (JSON.stringify(configuredNodeFiles) !== JSON.stringify(expectedNodeFiles)) { - errors.push( - `${path} must derive Node setups from ${expectedNodeFiles.join(', ')} in that order` - ) + if ( + configuredNodeFiles.length === 0 || + configuredNodeFiles.some((nodeFile) => nodeFile !== expectedNodeFile) + ) { + errors.push(`${path} must derive every Node setup from ${expectedNodeFile}`) } } if (rootPackage.devDependencies?.portless !== '0.12.0') { diff --git a/scripts/publish-native-extensions.sh b/scripts/publish-native-extensions.sh new file mode 100644 index 00000000..db9d3157 --- /dev/null +++ b/scripts/publish-native-extensions.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${INKCRE_EXTENSION_REGISTRY_TOKEN:?publisher token is required}" +: "${INKCRE_EXTENSION_REGISTRY_URL:?registry URL is required}" +: "${SOURCE_REPOSITORY:?source repository is required}" +: "${SOURCE_REVISION:?source revision is required}" + +workspace=${GITHUB_WORKSPACE:-$PWD} +summary=${GITHUB_STEP_SUMMARY:-/dev/stdout} +build_id="client-web-extension-release-${GITHUB_RUN_ID:-local}" +mkdir -p .extension-delivery + +for package_path in extensions/*/package.json; do + descriptor="$(node --input-type=module --eval ' + import { readFile } from "node:fs/promises" + import path from "node:path" + const packagePath = process.argv[1] + const manifest = JSON.parse(await readFile(packagePath, "utf8")) + if (!manifest.inkcre?.module_federation) process.exit(0) + process.stdout.write([path.basename(path.dirname(packagePath)), manifest.inkcre.name, manifest.version].join("\t")) + ' "$package_path")" + if [[ -z "$descriptor" ]]; then + continue + fi + IFS=$'\t' read -r extension_directory extension_path release_path <<< "$descriptor" + + delivery_directory=".extension-delivery/$extension_directory" + artifact_directory="extensions/$extension_directory/dist/client-web" + mkdir -p "$delivery_directory" + public_descriptor="$delivery_directory/public-release.json" + status="$(curl --silent --show-error --output "$public_descriptor" --write-out '%{http_code}' \ + "${INKCRE_EXTENSION_REGISTRY_URL%/}/v1/extensions/${extension_path}/releases/${release_path}")" + if [[ "$status" == '200' ]]; then + has_native_association="$(node --input-type=module --eval 'import { readFile } from "node:fs/promises"; const release = JSON.parse(await readFile(process.argv[1], "utf8")); process.stdout.write(String(release.module_federation !== null))' "$public_descriptor")" + elif [[ "$status" == '404' ]]; then + has_native_association=false + else + echo "Cannot determine $extension_path $release_path Registry state: HTTP $status." >&2 + exit 1 + fi + if [[ "$has_native_association" == 'true' ]]; then + echo "$extension_path $release_path is already published." >> "$summary" + continue + fi + + node scripts/verify-native-extension-distribution.mjs prepare-body \ + --package "$package_path" \ + --core-package packages/core/package.json \ + --artifact-directory "$artifact_directory" \ + --source-repository "$SOURCE_REPOSITORY" \ + --source-revision "$SOURCE_REVISION" \ + --build-id "$build_id" \ + --output "$delivery_directory/prepare.json" + ( + cd "$artifact_directory" + zip -q -r "$workspace/$delivery_directory/snapshot.zip" . + ) + + curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $INKCRE_EXTENSION_REGISTRY_TOKEN" \ + --header 'Content-Type: application/json' \ + --data-binary "@$delivery_directory/prepare.json" \ + "${INKCRE_EXTENSION_REGISTRY_URL%/}/v1/extensions/${extension_path}/releases" \ + > "$delivery_directory/prepared-release.json" + curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $INKCRE_EXTENSION_REGISTRY_TOKEN" \ + --form "content=@$delivery_directory/snapshot.zip;type=application/zip" \ + "${INKCRE_EXTENSION_REGISTRY_URL%/}/v1/extensions/${extension_path}/releases/${release_path}/module-federation" \ + > "$delivery_directory/uploaded-release.json" + curl --fail-with-body --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $INKCRE_EXTENSION_REGISTRY_TOKEN" \ + "${INKCRE_EXTENSION_REGISTRY_URL%/}/v1/extensions/${extension_path}/releases/${release_path}/publish" \ + > "$delivery_directory/published-release.json" + node scripts/verify-native-extension-distribution.mjs verify-public \ + --registry-url "$INKCRE_EXTENSION_REGISTRY_URL" \ + --package "$package_path" \ + --core-package packages/core/package.json \ + --artifact-directory "$artifact_directory" \ + --output "$delivery_directory/verification.json" + echo "$extension_path $release_path published and verified." >> "$summary" +done From 3474efd2d0589e5a833b0155210633b0d46ffa07 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Tue, 1 Sep 2026 11:17:04 +0800 Subject: [PATCH 2/2] chore(ci): drop unconsumed build artifact - keep failure-only browser and database diagnostics - stop retaining successful extension builds without a consumer --- .github/workflows/ci.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f663dc7..d0fe0115 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -210,16 +210,6 @@ jobs: - name: Build the Firefox extension run: pnpm --filter @inkcre/client-webext build:firefox - - name: Retain the exact browser-extension artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: client-webext-builds - path: | - apps/client-webext/.output/chrome-mv3 - apps/client-webext/.output/firefox-mv2 - if-no-files-found: error - retention-days: 7 - - name: Upload browser evidence on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1