From 3c7a9933e760ddd2113684fa9460462045d2d9d8 Mon Sep 17 00:00:00 2001 From: "commit-action-bot[bot]" <248498966+commit-action-bot[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 09:40:56 +0000 Subject: [PATCH] chore: deploy 0.3.1-rc10 --- .devcontainer/CHANGELOG.md | 14 +- .github/workflows/release.yml | 67 ++++++++- .github/workflows/repository-dispatch.yml | 167 +++------------------- .github/workflows/sync-main-to-dev.yml | 15 +- .vig-os | 2 +- CHANGELOG.md | 21 +-- uv.lock | 12 +- 7 files changed, 101 insertions(+), 197 deletions(-) diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index 02b54e2..756973d 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -60,14 +60,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Handle smoke-test dispatch failures with a targeted issue while avoiding destructive rollback after publish artifacts are already released - **Redesigned smoke-test dispatch release orchestration** ([#358](https://github.com/vig-os/devcontainer/issues/358)) - Replace premature `publish-release` behavior with full downstream orchestration: deploy-to-dev merge gate, `prepare-release.yml`, release PR readiness/approval, and `release.yml` dispatch polling - - Add release-branch CHANGELOG sync so smoke-test `main` ends with the same `CHANGELOG.md` content as `vig-os/devcontainer` at the dispatched tag - Add upstream failure issue reporting with job-phase results and cleanup guidance when dispatch orchestration fails +- **Smoke-test release orchestration now runs as two phases** ([#402](https://github.com/vig-os/devcontainer/issues/402)) + - Keep `repository-dispatch.yml` focused on deploy/prepare/release-PR readiness and move release dispatch to a dedicated merged-PR workflow (`on-release-pr-merge.yml`) + - Add release-kind labeling and auto-merge enablement for release PRs, and keep upstream failure notifications in both phases + - Remove release-branch upstream `CHANGELOG.md` sync from `repository-dispatch.yml` (previously added in [#358](https://github.com/vig-os/devcontainer/issues/358)) ### Fixed - **Release app permission docs now include downstream workflow dispatch requirements** ([#397](https://github.com/vig-os/devcontainer/issues/397)) - Update `docs/RELEASE_CYCLE.md` to require `Actions` read/write for `RELEASE_APP` on the validation repository - Clarify this is required so downstream `repository-dispatch.yml` can trigger release orchestration workflows via `workflow_dispatch` +- **Smoke-test dispatch no longer fails on release PR self-approval** ([#402](https://github.com/vig-os/devcontainer/issues/402)) + - Remove bot self-approval from `repository-dispatch.yml` and replace with release-kind labeling plus auto-merge enablement + - Remove in-job polling for release PR merge and downstream release execution from phase 1 orchestration + - Phase 2 (`on-release-pr-merge.yml`) fails validation unless the merged release PR has `release-kind:final` or `release-kind:candidate` +- **Sync-main-to-dev PRs now trigger CI reliably in downstream repos** ([#398](https://github.com/vig-os/devcontainer/issues/398)) + - Replace API-based sync branch creation with `git push` in `assets/workspace/.github/workflows/sync-main-to-dev.yml` +- **Sync-main-to-dev no longer dispatches CI via workflow_dispatch** ([#405](https://github.com/vig-os/devcontainer/issues/405)) + - `workflow_dispatch` runs are omitted from the PR status check rollup, so they do not satisfy branch protection on the sync PR + - Remove the post-PR `gh workflow run ci.yml` step and drop `actions: write` from the sync job in `.github/workflows/sync-main-to-dev.yml` and `assets/workspace/.github/workflows/sync-main-to-dev.yml` - **Release finalization now commits generated docs and refreshes PR content** ([#300](https://github.com/vig-os/devcontainer/issues/300)) - Final release automation regenerates docs before committing so pre-commit `generate-docs` does not fail CI with tracked file diffs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d0a582..d2bb29a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,9 +36,32 @@ permissions: contents: read jobs: + resolve-image: + name: Resolve image tag + runs-on: ubuntu-22.04 + timeout-minutes: 2 + outputs: + image-tag: ${{ steps.resolve.outputs.image-tag }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .vig-os + .github/actions/resolve-image + sparse-checkout-cone-mode: false + + - name: Resolve container image + id: resolve + uses: ./.github/actions/resolve-image + core: name: Release Core uses: ./.github/workflows/release-core.yml + permissions: + actions: write + contents: write + pull-requests: read with: version: ${{ inputs.version }} release_kind: ${{ inputs.release-kind }} @@ -65,6 +88,8 @@ jobs: needs: [core, extension] if: ${{ inputs.dry-run != true }} uses: ./.github/workflows/release-publish.yml + permissions: + contents: write with: version: ${{ needs.core.outputs.version }} finalize_sha: ${{ needs.core.outputs.finalize_sha }} @@ -77,21 +102,55 @@ jobs: rollback: name: Rollback on Failure - needs: [core, extension, publish] + needs: [resolve-image, core, extension, publish] runs-on: ubuntu-22.04 container: - image: ghcr.io/vig-os/devcontainer:${{ needs.core.outputs.image_tag }} + image: ghcr.io/vig-os/devcontainer:${{ needs.resolve-image.outputs.image-tag }} timeout-minutes: 10 - if: ${{ failure() && inputs.dry-run != true }} + defaults: + run: + shell: bash + if: >- + ${{ + always() && + inputs.dry-run != true && + needs.resolve-image.result == 'success' && + ( + needs.core.result == 'failure' || + needs.extension.result == 'failure' || + needs.publish.result == 'failure' + ) + }} permissions: contents: write issues: write steps: + - name: Generate release app token + id: release_app_token + if: ${{ needs.core.outputs.version != '' }} + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + with: + app-id: ${{ secrets.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + + - name: Generate commit app token + id: commit_app_token + if: ${{ needs.core.outputs.pre_finalize_sha != '' }} + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + with: + app-id: ${{ secrets.COMMIT_APP_ID }} + private-key: ${{ secrets.COMMIT_APP_PRIVATE_KEY }} + - name: Checkout repository if: ${{ needs.core.outputs.pre_finalize_sha != '' }} uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + token: ${{ steps.commit_app_token.outputs.token }} + + - name: Fix git safe.directory + if: ${{ needs.core.outputs.pre_finalize_sha != '' }} + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Configure git if: ${{ needs.core.outputs.pre_finalize_sha != '' }} @@ -134,7 +193,7 @@ jobs: env: VERSION: ${{ needs.core.outputs.version }} PR_NUMBER: ${{ needs.core.outputs.pr_number }} - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.release_app_token.outputs.token }} run: | set -euo pipefail WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.github/workflows/repository-dispatch.yml b/.github/workflows/repository-dispatch.yml index 88eac6d..4026572 100644 --- a/.github/workflows/repository-dispatch.yml +++ b/.github/workflows/repository-dispatch.yml @@ -482,7 +482,7 @@ jobs: exit 1 ready-release-pr: - name: Sync changelog and prepare release PR + name: Prepare release PR runs-on: ubuntu-22.04 timeout-minutes: 35 env: @@ -492,7 +492,7 @@ jobs: release_pr: ${{ steps.locate_release_pr.outputs.release_pr }} release_pr_url: ${{ steps.locate_release_pr.outputs.release_pr_url }} steps: - - name: Generate release app token for PR and contents operations + - name: Generate release app token for PR operations id: generate_release_token uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 with: @@ -501,41 +501,6 @@ jobs: owner: ${{ github.repository_owner }} repositories: ${{ github.event.repository.name }} - - name: Sync upstream CHANGELOG onto release branch - env: - GH_TOKEN: ${{ steps.generate_release_token.outputs.token }} - TAG: ${{ needs.validate.outputs.tag }} - BASE_VERSION: ${{ needs.validate.outputs.base_version }} - run: | - set -euo pipefail - CHANGELOG_URL="https://raw.githubusercontent.com/vig-os/devcontainer/${TAG}/CHANGELOG.md" - ATTEMPT=1 - MAX_ATTEMPTS=3 - until [ "${ATTEMPT}" -gt "${MAX_ATTEMPTS}" ]; do - if curl -sSf "${CHANGELOG_URL}" -o /tmp/upstream-changelog.md; then - break - fi - if [ "${ATTEMPT}" -eq "${MAX_ATTEMPTS}" ]; then - echo "ERROR: failed to download upstream changelog after ${MAX_ATTEMPTS} attempts: ${CHANGELOG_URL}" - exit 1 - fi - echo "Changelog download attempt ${ATTEMPT}/${MAX_ATTEMPTS} failed; retrying in 5s..." - ATTEMPT=$((ATTEMPT + 1)) - sleep 5 - done - - FILE_SHA="$(gh api \ - "repos/${GITHUB_REPOSITORY}/contents/CHANGELOG.md?ref=release/${BASE_VERSION}" \ - --jq '.sha')" - CONTENT="$(base64 -w0 < /tmp/upstream-changelog.md)" - - gh api -X PUT "repos/${GITHUB_REPOSITORY}/contents/CHANGELOG.md" \ - -f message="chore: sync CHANGELOG from devcontainer ${TAG}" \ - -f content="${CONTENT}" \ - -f sha="${FILE_SHA}" \ - -f branch="release/${BASE_VERSION}" >/dev/null - echo "Synced upstream CHANGELOG to release/${BASE_VERSION}" - - name: Locate release PR id: locate_release_pr env: @@ -552,7 +517,7 @@ jobs: echo "release_pr=${PR_NUMBER}" >> "${GITHUB_OUTPUT}" echo "release_pr_url=${PR_URL}" >> "${GITHUB_OUTPUT}" - - name: Mark release PR ready and approve + - name: Mark release PR ready env: GH_TOKEN: ${{ steps.generate_release_token.outputs.token }} PR_NUMBER: ${{ steps.locate_release_pr.outputs.release_pr }} @@ -562,121 +527,30 @@ jobs: if [ "${IS_DRAFT}" = "true" ]; then gh pr ready "${PR_NUMBER}" fi - gh pr review "${PR_NUMBER}" --approve - - name: Wait for release PR CI and merge + - name: Label release PR with release kind env: GH_TOKEN: ${{ steps.generate_release_token.outputs.token }} PR_NUMBER: ${{ steps.locate_release_pr.outputs.release_pr }} - run: | - set -euo pipefail - TIMEOUT=1800 - INTERVAL=30 - ELAPSED=0 - AUTO_MERGE_SET=false - - while [ "${ELAPSED}" -lt "${TIMEOUT}" ]; do - PR_STATE="$(gh pr view "${PR_NUMBER}" --json state --jq '.state' 2>/dev/null || echo unknown)" - if [ "${PR_STATE}" = "MERGED" ]; then - echo "Release PR merged: #${PR_NUMBER}" - exit 0 - fi - if [ "${PR_STATE}" = "CLOSED" ]; then - echo "ERROR: release PR closed without merge: #${PR_NUMBER}" - exit 1 - fi - - MERGE_STATE="$(gh pr view "${PR_NUMBER}" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null || echo unknown)" - if [ "${MERGE_STATE}" = "CLEAN" ] && [ "${AUTO_MERGE_SET}" = "false" ]; then - if gh pr merge "${PR_NUMBER}" --auto --merge; then - AUTO_MERGE_SET=true - else - echo "Warning: could not enable auto-merge yet; will retry" - fi - fi - - sleep "${INTERVAL}" - ELAPSED=$((ELAPSED + INTERVAL)) - echo "Waiting for release PR merge... (${ELAPSED}s/${TIMEOUT}s)" - done - - echo "ERROR: timed out waiting for release PR merge" - exit 1 - - trigger-release: - name: Trigger and wait for release workflow - runs-on: ubuntu-22.04 - timeout-minutes: 35 - env: - GH_REPO: ${{ github.repository }} - needs: [validate, ready-release-pr] - outputs: - before_run_id: ${{ steps.capture_release_before.outputs.before_run_id }} - steps: - - name: Generate release app token for release workflow dispatch - id: generate_release_token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 - with: - app-id: ${{ secrets.RELEASE_APP_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - repositories: ${{ github.event.repository.name }} - - - name: Capture latest release run id - id: capture_release_before - env: - GH_TOKEN: ${{ steps.generate_release_token.outputs.token }} - run: | - set -euo pipefail - BEFORE_RUN_ID="$( - gh run list --workflow release.yml --branch "${WORKFLOW_REF}" --limit 1 --json databaseId --jq '.[0].databaseId // 0' 2>/dev/null || echo 0 - )" - echo "before_run_id=${BEFORE_RUN_ID}" >> "${GITHUB_OUTPUT}" - - - name: Trigger release workflow - env: - GH_TOKEN: ${{ steps.generate_release_token.outputs.token }} - BASE_VERSION: ${{ needs.validate.outputs.base_version }} RELEASE_KIND: ${{ needs.validate.outputs.release_kind }} run: | set -euo pipefail - gh workflow run release.yml \ - --ref "${WORKFLOW_REF}" \ - -f version="${BASE_VERSION}" \ - -f release-kind="${RELEASE_KIND}" - - - name: Wait for release workflow completion + LABEL="release-kind:${RELEASE_KIND}" + gh label create "${LABEL}" --color "5319E7" \ + --description "Automated release kind label for dispatch orchestration" \ + --force >/dev/null 2>&1 || true + gh pr edit "${PR_NUMBER}" --remove-label "release-kind:candidate" >/dev/null 2>&1 || true + gh pr edit "${PR_NUMBER}" --remove-label "release-kind:final" >/dev/null 2>&1 || true + gh pr edit "${PR_NUMBER}" --add-label "${LABEL}" + + - name: Enable release PR auto-merge env: GH_TOKEN: ${{ steps.generate_release_token.outputs.token }} - BEFORE_RUN_ID: ${{ steps.capture_release_before.outputs.before_run_id }} + PR_NUMBER: ${{ steps.locate_release_pr.outputs.release_pr }} run: | set -euo pipefail - TIMEOUT=1800 - INTERVAL=30 - ELAPSED=0 - - while [ "${ELAPSED}" -lt "${TIMEOUT}" ]; do - RUN_ID="$(gh run list --workflow release.yml --branch "${WORKFLOW_REF}" --limit 1 --json databaseId --jq '.[0].databaseId // empty' 2>/dev/null || true)" - if [ -n "${RUN_ID}" ] && [ "${RUN_ID}" -gt "${BEFORE_RUN_ID}" ]; then - STATUS="$(gh run view "${RUN_ID}" --json status --jq '.status' 2>/dev/null || echo unknown)" - if [ "${STATUS}" = "completed" ]; then - CONCLUSION="$(gh run view "${RUN_ID}" --json conclusion --jq '.conclusion' 2>/dev/null || echo unknown)" - if [ "${CONCLUSION}" != "success" ]; then - echo "ERROR: release workflow concluded with '${CONCLUSION}'" - exit 1 - fi - echo "release workflow completed successfully" - exit 0 - fi - fi - - sleep "${INTERVAL}" - ELAPSED=$((ELAPSED + INTERVAL)) - echo "Waiting for release workflow... (${ELAPSED}s/${TIMEOUT}s)" - done - - echo "ERROR: timed out waiting for release workflow completion" - exit 1 + gh pr merge "${PR_NUMBER}" --auto --merge || \ + echo "Warning: could not enable auto-merge yet" summary: name: Dispatch summary @@ -689,7 +563,6 @@ jobs: - cleanup-release - trigger-prepare-release - ready-release-pr - - trigger-release if: always() steps: - name: Write source context summary @@ -725,7 +598,6 @@ jobs: echo "Cleanup: ${{ needs.cleanup-release.result }}" echo "Prepare: ${{ needs.trigger-prepare-release.result }}" echo "Release PR: ${{ needs.ready-release-pr.result }}" - echo "Release: ${{ needs.trigger-release.result }}" echo "Deploy PR: ${{ needs.deploy.outputs.pr_url }}" echo "Release PR: ${{ needs.ready-release-pr.outputs.release_pr_url }}" echo "" @@ -762,11 +634,6 @@ jobs: FAILED=true fi - if [ "${{ needs.trigger-release.result }}" != "success" ]; then - echo "ERROR: Release workflow orchestration job failed" - FAILED=true - fi - if [ "${FAILED}" = "true" ]; then echo "" echo "Dispatch orchestration failed" @@ -788,7 +655,6 @@ jobs: - cleanup-release - trigger-prepare-release - ready-release-pr - - trigger-release - summary steps: - name: Generate release app token for upstream issue creation @@ -843,7 +709,6 @@ jobs: - cleanup-release: \`${{ needs.cleanup-release.result }}\` - trigger-prepare-release: \`${{ needs.trigger-prepare-release.result }}\` - ready-release-pr: \`${{ needs.ready-release-pr.result }}\` - - trigger-release: \`${{ needs.trigger-release.result }}\` - summary: \`${{ needs.summary.result }}\` ## Manual cleanup guidance diff --git a/.github/workflows/sync-main-to-dev.yml b/.github/workflows/sync-main-to-dev.yml index 223a72f..c38192f 100644 --- a/.github/workflows/sync-main-to-dev.yml +++ b/.github/workflows/sync-main-to-dev.yml @@ -9,9 +9,7 @@ # sync - clean up stale sync branches # - trial merge to detect conflicts # - create chore/sync-main-to-dev-- branch via git push -# - open PR, then workflow_dispatch CI on sync branch (before auto-merge; -# runs for conflict PRs too) -# - enable auto-merge when clean, or label "merge-conflict" with +# - open PR; enable auto-merge when clean, or label "merge-conflict" with # resolution instructions when conflicts exist # # Auth: Two GitHub App tokens: @@ -118,7 +116,6 @@ jobs: env: SYNC_BRANCH: chore/sync-main-to-dev-${{ github.run_number }}-${{ github.run_attempt }} permissions: - actions: write contents: write pull-requests: write issues: write @@ -274,16 +271,6 @@ jobs: echo "Warning: failed to add merge-conflict label." fi - - name: Trigger CI on sync branch - if: steps.create-pr.outputs.pr_url != '' - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - retry --retries 3 --backoff 5 --max-backoff 30 -- \ - gh workflow run ci.yml --ref "${SYNC_BRANCH}" - echo "Dispatched CI workflow on ${SYNC_BRANCH}" - - name: Enable auto-merge if: >- steps.create-pr.outputs.pr_url != '' diff --git a/.vig-os b/.vig-os index 02ee625..1f00765 100644 --- a/.vig-os +++ b/.vig-os @@ -1,2 +1,2 @@ # vig-os devcontainer configuration -DEVCONTAINER_VERSION=0.3.1-rc9 +DEVCONTAINER_VERSION=0.3.1-rc10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2537d63..7262d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,7 @@ # Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - ## Unreleased -### Added - -### Changed - -### Deprecated - -### Removed - -### Fixed - -### Security - -## [0.3.1] - TBD - ### Changed -- Deploy devcontainer 0.3.1-rc9 +- Deploy devcontainer 0.3.1-rc10 diff --git a/uv.lock b/uv.lock index db79590..dd063d6 100644 --- a/uv.lock +++ b/uv.lock @@ -822,11 +822,11 @@ wheels = [ [[package]] name = "jsonpointer" -version = "3.0.0" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/bf/9ecc036fbc15cf4153ea6ed4dbeed31ef043f762cccc9d44a534be8319b0/jsonpointer-3.1.0.tar.gz", hash = "sha256:f9b39abd59ba8c1de8a4ff16141605d2a8dacc4dd6cf399672cf237bfe47c211", size = 9000, upload-time = "2026-03-20T21:47:09.982Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/cebb241a435cbf4626b5ea096d8385c04416d7ca3082a15299b746e248fa/jsonpointer-3.1.0-py3-none-any.whl", hash = "sha256:f82aa0f745001f169b96473348370b43c3f581446889c41c807bab1db11c8e7b", size = 7651, upload-time = "2026-03-20T21:47:08.792Z" }, ] [[package]] @@ -1773,16 +1773,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]]