Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
570ed2183b | ||
|
|
d7a74a6971 | ||
|
|
18963ae03b | ||
|
|
f8c186fe2c | ||
|
|
d40d19ae96 | ||
|
|
ac17a954b0 | ||
|
|
72fd363600 | ||
|
|
044c83cd22 | ||
|
|
121b835494 | ||
|
|
5ae9f94023 | ||
|
|
263232bea4 |
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# bark-notify.sh — push a notification to the local Bark server.
|
||||||
|
#
|
||||||
|
# Usage: bark-notify.sh <title> <body>
|
||||||
|
#
|
||||||
|
# The Bark endpoint, device_key, and group are pinned in this script
|
||||||
|
# (it is a deployment detail, not a secret). The group is "Gitea" —
|
||||||
|
# change it here if you want to sort notifications by category.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "$#" -ne 2 ]; then
|
||||||
|
echo "usage: $0 <title> <body>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TITLE="$1"
|
||||||
|
BODY="$2"
|
||||||
|
|
||||||
|
PAYLOAD=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"device_key": "6ZFVsxM95cuAjYoNLWSWaN",
|
||||||
|
"title": "${TITLE}",
|
||||||
|
"body": "${BODY}",
|
||||||
|
"group": "Gitea",
|
||||||
|
"isArchive": 1,
|
||||||
|
"ttl": 3600
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
curl -s \
|
||||||
|
-X POST http://101.43.40.124:81/push \
|
||||||
|
-H "Content-Type: application/json; charset=utf-8" \
|
||||||
|
-d "${PAYLOAD}"
|
||||||
|
|
||||||
|
echo "bark notified: ${TITLE}"
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# create-release.sh — create a Gitea release and attach dist/ assets.
|
||||||
|
#
|
||||||
|
# Required env vars (set by the workflow):
|
||||||
|
# GITEA_TOKEN Gitea API token with release write scope
|
||||||
|
# TAG_NAME tag name (e.g. v0.0.1-beta)
|
||||||
|
# API_URL Gitea API base URL (e.g. http://gitea.local/api/v1)
|
||||||
|
# REPOSITORY owner/repo
|
||||||
|
# SHA commit SHA the tag points at
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PAYLOAD=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"tag_name": "${TAG_NAME}",
|
||||||
|
"target_commitish": "${SHA}",
|
||||||
|
"name": "Release ${TAG_NAME}",
|
||||||
|
"body": "Auto-generated release assets.",
|
||||||
|
"draft": false,
|
||||||
|
"prerelease": false
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Creating Gitea release for tag ${TAG_NAME}..."
|
||||||
|
|
||||||
|
RELEASE_RESPONSE=$(curl -fsS -X POST "${API_URL}/repos/${REPOSITORY}/releases" \
|
||||||
|
-H "accept: application/json" \
|
||||||
|
-H "authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "${PAYLOAD}")
|
||||||
|
|
||||||
|
RELEASE_ID=$(printf '%s' "${RELEASE_RESPONSE}" | grep -oP '"id":\s*\K[0-9]+' | head -n 1)
|
||||||
|
if [ -z "${RELEASE_ID}" ]; then
|
||||||
|
echo "ERROR: failed to create release. Response:"
|
||||||
|
echo "${RELEASE_RESPONSE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Release created with ID ${RELEASE_ID}"
|
||||||
|
|
||||||
|
for asset in dist/snapshot-*.whl dist/snapshot-*.tar.gz; do
|
||||||
|
if [ -f "${asset}" ]; then
|
||||||
|
echo "Uploading ${asset}..."
|
||||||
|
curl -fsS -X POST "${API_URL}/repos/${REPOSITORY}/releases/${RELEASE_ID}/assets" \
|
||||||
|
-H "accept: application/json" \
|
||||||
|
-H "authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-F "attachment=@${asset}"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo "Skipping ${asset} (not found)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
+52
-150
@@ -1,162 +1,64 @@
|
|||||||
name: Build
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: main
|
# branches:
|
||||||
|
# - main
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: '*'
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
ci:
|
||||||
|
name: CI
|
||||||
|
# Standard GitHub-hosted ubuntu runner. uv is installed via the
|
||||||
|
# astral-sh/setup-uv action (one extra step vs. using a self-hosted
|
||||||
|
# runner with uv pre-installed, but matches what GitHub-hosted CI
|
||||||
|
# provides out of the box).
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Base Setup
|
- name: Set up uv
|
||||||
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
|
uses: https://github.com/astral-sh/setup-uv@v6
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Build package
|
||||||
run: python -m pip install -U "jupyterlab>=4.0.0,<5"
|
env:
|
||||||
|
# Use the Tsinghua PyPI mirror for build-environment resolution;
|
||||||
|
# the runner is in China and the default pypi.org is too slow
|
||||||
|
# / unreliable for build dependency install.
|
||||||
|
UV_DEFAULT_INDEX: "https://pypi.tuna.tsinghua.edu.cn/simple/"
|
||||||
|
run: uv build
|
||||||
|
|
||||||
- name: Lint the extension
|
# Only create a release + attach dist/ assets when pushing a v* tag.
|
||||||
run: |
|
# On branch pushes / pull requests this step is skipped entirely.
|
||||||
set -eux
|
# The release logic is in a separate script to avoid embedding a JSON
|
||||||
jlpm
|
# heredoc inside a YAML block scalar (the previous version of this
|
||||||
jlpm run lint:check
|
# step was rejected by Gitea's YAML parser at line 51).
|
||||||
|
- name: Upload release assets via Gitea API
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
|
API_URL: ${{ github.api_url }}
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
|
SHA: ${{ github.sha }}
|
||||||
|
run: ./.github/scripts/create-release.sh
|
||||||
|
|
||||||
- name: Test the extension
|
# Always run, regardless of job status. The script is a side-effect;
|
||||||
run: |
|
# if the notification itself fails we swallow the error so CI does
|
||||||
set -eux
|
# not turn red on a Bark outage.
|
||||||
jlpm run test
|
- name: Notify Bark
|
||||||
|
if: always()
|
||||||
- name: Build the extension
|
run: |
|
||||||
run: |
|
if [ "${{ job.status }}" = "success" ]; then
|
||||||
set -eux
|
TITLE="✅ CI: ${{ github.repository }}@${{ github.ref_name }}"
|
||||||
python -m pip install .[test]
|
BODY="Build OK for ${{ github.sha }} by ${{ github.actor }}"
|
||||||
|
else
|
||||||
pytest -vv -r ap --cov snapshot
|
TITLE="❌ CI: ${{ github.repository }}@${{ github.ref_name }}"
|
||||||
jupyter server extension list
|
BODY="Build FAILED for ${{ github.sha }} by ${{ github.actor }}"
|
||||||
jupyter server extension list 2>&1 | grep -ie "snapshot.*OK"
|
fi
|
||||||
|
./.github/scripts/bark-notify.sh "$TITLE" "$BODY" || echo "Bark notify failed (non-fatal)"
|
||||||
jupyter labextension list
|
|
||||||
jupyter labextension list 2>&1 | grep -ie "snapshot.*OK"
|
|
||||||
python -m jupyterlab.browser_check
|
|
||||||
|
|
||||||
- name: Package the extension
|
|
||||||
run: |
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
pip install build
|
|
||||||
python -m build
|
|
||||||
pip uninstall -y "snapshot" jupyterlab
|
|
||||||
|
|
||||||
- name: Upload extension packages
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: extension-artifacts
|
|
||||||
path: dist/snapshot*
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
test_isolated:
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Install Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.10'
|
|
||||||
architecture: 'x64'
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: extension-artifacts
|
|
||||||
- name: Install and Test
|
|
||||||
run: |
|
|
||||||
set -eux
|
|
||||||
# Remove NodeJS, twice to take care of system and locally installed node versions.
|
|
||||||
sudo rm -rf $(which node)
|
|
||||||
sudo rm -rf $(which node)
|
|
||||||
|
|
||||||
pip install "jupyterlab>=4.0.0,<5" snapshot*.whl
|
|
||||||
|
|
||||||
|
|
||||||
jupyter server extension list
|
|
||||||
jupyter server extension list 2>&1 | grep -ie "snapshot.*OK"
|
|
||||||
|
|
||||||
jupyter labextension list
|
|
||||||
jupyter labextension list 2>&1 | grep -ie "snapshot.*OK"
|
|
||||||
python -m jupyterlab.browser_check --no-browser-test
|
|
||||||
|
|
||||||
integration-tests:
|
|
||||||
name: Integration tests
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Base Setup
|
|
||||||
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
|
|
||||||
|
|
||||||
- name: Download extension package
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: extension-artifacts
|
|
||||||
|
|
||||||
- name: Install the extension
|
|
||||||
run: |
|
|
||||||
set -eux
|
|
||||||
python -m pip install "jupyterlab>=4.0.0,<5" snapshot*.whl
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
working-directory: ui-tests
|
|
||||||
env:
|
|
||||||
YARN_ENABLE_IMMUTABLE_INSTALLS: 0
|
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
|
||||||
run: jlpm install
|
|
||||||
|
|
||||||
- name: Set up browser cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ github.workspace }}/pw-browsers
|
|
||||||
key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }}
|
|
||||||
|
|
||||||
- name: Install browser
|
|
||||||
run: |
|
|
||||||
set -eux
|
|
||||||
jlpm playwright install chromium --only-shell
|
|
||||||
working-directory: ui-tests
|
|
||||||
|
|
||||||
- name: Execute integration tests
|
|
||||||
working-directory: ui-tests
|
|
||||||
run: |
|
|
||||||
jlpm playwright test
|
|
||||||
|
|
||||||
- name: Upload Playwright Test report
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: snapshot-playwright-tests
|
|
||||||
path: |
|
|
||||||
ui-tests/test-results
|
|
||||||
ui-tests/playwright-report
|
|
||||||
|
|
||||||
check_links:
|
|
||||||
name: Check Links
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
|
|
||||||
- uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
name: Check Release
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["*"]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check_release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Base Setup
|
|
||||||
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
|
|
||||||
- name: Check Release
|
|
||||||
uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2
|
|
||||||
with:
|
|
||||||
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Upload Distributions
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: snapshot-releaser-dist-${{ github.run_number }}
|
|
||||||
path: .jupyter_releaser_checkout/dist
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
name: Enforce PR label
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [labeled, unlabeled, opened, edited, synchronize]
|
|
||||||
jobs:
|
|
||||||
enforce-label:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: enforce-triage-label
|
|
||||||
uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
name: "Step 1: Prep Release"
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version_spec:
|
|
||||||
description: "New Version Specifier"
|
|
||||||
default: "next"
|
|
||||||
required: false
|
|
||||||
branch:
|
|
||||||
description: "The branch to target"
|
|
||||||
required: false
|
|
||||||
post_version_spec:
|
|
||||||
description: "Post Version Specifier"
|
|
||||||
required: false
|
|
||||||
# silent:
|
|
||||||
# description: "Set a placeholder in the changelog and don't publish the release."
|
|
||||||
# required: false
|
|
||||||
# type: boolean
|
|
||||||
since:
|
|
||||||
description: "Use PRs with activity since this date or git reference"
|
|
||||||
required: false
|
|
||||||
since_last_stable:
|
|
||||||
description: "Use PRs with activity since the last stable git tag"
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
jobs:
|
|
||||||
prep_release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
|
|
||||||
|
|
||||||
- name: Prep Release
|
|
||||||
id: prep-release
|
|
||||||
uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
version_spec: ${{ github.event.inputs.version_spec }}
|
|
||||||
# silent: ${{ github.event.inputs.silent }}
|
|
||||||
post_version_spec: ${{ github.event.inputs.post_version_spec }}
|
|
||||||
branch: ${{ github.event.inputs.branch }}
|
|
||||||
since: ${{ github.event.inputs.since }}
|
|
||||||
since_last_stable: ${{ github.event.inputs.since_last_stable }}
|
|
||||||
|
|
||||||
- name: "** Next Step **"
|
|
||||||
run: |
|
|
||||||
echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}"
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
name: "Step 2: Publish Release"
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
branch:
|
|
||||||
description: "The target branch"
|
|
||||||
required: false
|
|
||||||
release_url:
|
|
||||||
description: "The URL of the draft GitHub release"
|
|
||||||
required: false
|
|
||||||
steps_to_skip:
|
|
||||||
description: "Comma separated list of steps to skip"
|
|
||||||
required: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish_release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: release
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
steps:
|
|
||||||
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
|
|
||||||
|
|
||||||
- uses: actions/create-github-app-token@v1
|
|
||||||
id: app-token
|
|
||||||
with:
|
|
||||||
app-id: ${{ vars.APP_ID }}
|
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
||||||
|
|
||||||
- name: Populate Release
|
|
||||||
id: populate-release
|
|
||||||
uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2
|
|
||||||
with:
|
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
|
||||||
branch: ${{ github.event.inputs.branch }}
|
|
||||||
release_url: ${{ github.event.inputs.release_url }}
|
|
||||||
steps_to_skip: ${{ github.event.inputs.steps_to_skip }}
|
|
||||||
|
|
||||||
- name: Finalize Release
|
|
||||||
id: finalize-release
|
|
||||||
env:
|
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
|
|
||||||
with:
|
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
|
||||||
release_url: ${{ steps.populate-release.outputs.release_url }}
|
|
||||||
|
|
||||||
- name: "** Next Step **"
|
|
||||||
if: ${{ success() }}
|
|
||||||
run: |
|
|
||||||
echo "Verify the final release"
|
|
||||||
echo ${{ steps.finalize-release.outputs.release_url }}
|
|
||||||
|
|
||||||
- name: "** Failure Message **"
|
|
||||||
if: ${{ failure() }}
|
|
||||||
run: |
|
|
||||||
echo "Failed to Publish the Draft Release Url:"
|
|
||||||
echo ${{ steps.populate-release.outputs.release_url }}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: Update Playwright Snapshots
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created, edited]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-snapshots:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots-checkout@main
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Base Setup
|
|
||||||
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: python -m pip install -U "jupyterlab>=4.0.0,<5"
|
|
||||||
|
|
||||||
- name: Install extension
|
|
||||||
run: |
|
|
||||||
set -eux
|
|
||||||
jlpm
|
|
||||||
python -m pip install .
|
|
||||||
|
|
||||||
- uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# Playwright knows how to start JupyterLab server
|
|
||||||
start_server_script: 'null'
|
|
||||||
test_folder: ui-tests
|
|
||||||
npm_client: jlpm
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
待办项,**按用户需求驱动**——别凭空虚构。完成一项后:勾选 + 改 CHANGELOG + 在 commit message 引用本节。
|
||||||
|
|
||||||
|
> 用户视角(配置 / 安装)见 [README.md](README.md)。设计目标见 [design.md](design.md)。工程手册见 [DEVELOPER.md](DEVELOPER.md)。
|
||||||
|
|
||||||
|
## 后端
|
||||||
|
|
||||||
|
- [ ] **`POST /notebook-version/restore` 端点**(design.md 里标"暂缓")
|
||||||
|
- 现状:restore 走纯前端 `model.fromJSON()` + `context.save()`,只能恢复**当前打开的** notebook
|
||||||
|
- 端点能力:脚本/CLI 调 API 恢复任意路径的 notebook;删了的 notebook 从 S3 拉回
|
||||||
|
- 必须走 `ContentsManager.write`(不能直接写文件),要处理权限 / 文件锁 / 并发
|
||||||
|
- 估计:~50 行 Python + ~30 行测试
|
||||||
|
- 触发条件:团队有 CI 灾备 / 批量恢复需求
|
||||||
|
|
||||||
|
- [ ] **修复 S3 提交后 list 偶发返回旧 manifest**
|
||||||
|
- 根因:commit 写 manifest 后前端独立发 list;某些 S3 兼容服务(MinIO / Ceph RGW / 阿里云 OSS 等)实现各异,强一致性不保证
|
||||||
|
- 方案:把 list 塞进 commit 响应(commit 写完立刻在同进程内 list 一定是最新),省掉独立 GET
|
||||||
|
- 估计:~30 行(改 commit 响应 + 前端 `setVersions` 直写 + 测试)
|
||||||
|
- 临时缓解:侧边栏「刷新」按钮(已加)
|
||||||
|
|
||||||
|
## 前端
|
||||||
|
|
||||||
|
- [ ] **`schema/plugin.json` settings schema**
|
||||||
|
- 现状:空 schema,插件激活时只 `console.log` settings,无任何可配置项
|
||||||
|
- 候选配置:`confirmRestore` / `defaultDescription` / `maxVersionsToShow` / `commitShortcut` / `skipDuplicate`
|
||||||
|
- 估计:~100 行(schema.json + 前端读 settings + 一个真用上的配置项)
|
||||||
|
- 触发条件:真有用户想调参再加
|
||||||
|
|
||||||
|
- [ ] **修复 jest 在 pnpm 链接器下跑不起来**
|
||||||
|
- 根因:`.yarnrc.yml` 用 `yarnLinker: pnpm`,虚拟存储把 `ts-jest` 和 `jest-util` 隔开
|
||||||
|
- 当前绕过:`jlpm build`(tsc 严格 + rspack)验证前端
|
||||||
|
- 方案 A:改 `.yarnrc.yml` 切 `nodeLinker: node-modules`(影响整个 monorepo 解析)
|
||||||
|
- 方案 B:完全重写 `jest.config.js` 不依赖 `@jupyterlab/testing` 的 baseConfig
|
||||||
|
- 估计:A 1 行;B 半日
|
||||||
|
|
||||||
|
- [ ] **前端 diff 逻辑单测**
|
||||||
|
- `src/diff.ts` 是纯函数,最值得测;目前没有单测跑(受上面 jest 故障牵连)
|
||||||
|
- 估计:跟 jest 修复一起做,~50 行测试
|
||||||
|
|
||||||
|
## 测试 / CI
|
||||||
|
|
||||||
|
- [ ] **ui-tests(Playwright + Galata)跑通**
|
||||||
|
- 当前:`ui-tests/` 目录有但 README 里有 yarn.lock 是空的,从未跑过
|
||||||
|
- 前置:先把 jest 修通,CI 工作流 `.github/workflows/build.yml` 跑通
|
||||||
|
- 估计:跟 CI 一起做,~2-3 天
|
||||||
|
|
||||||
|
- [ ] **CI 流水线加后端 pytest**
|
||||||
|
- 当前 `.github/workflows/build.yml` 没看到 pytest 步骤
|
||||||
|
- 估计:~10 行 yaml
|
||||||
|
|
||||||
|
## 发布
|
||||||
|
|
||||||
|
- [ ] **v0.0.1 tag + 推送到 origin**
|
||||||
|
```bash
|
||||||
|
git tag v0.0.1
|
||||||
|
git push origin main --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **v0.0.1 wheel 发布到 PyPI**(如需)
|
||||||
|
- `python -m build` 出 wheel(hatch 自动嵌前端)
|
||||||
|
- `twine upload dist/*`
|
||||||
|
- 触发条件:要让别人 `pip install snapshot` 装上
|
||||||
|
|
||||||
|
## 优先级
|
||||||
|
|
||||||
|
按"真实价值"排:
|
||||||
|
|
||||||
|
1. 修 S3 一致性(用户已报告的 bug 根因)
|
||||||
|
2. v0.0.1 tag(当前未发版)
|
||||||
|
3. jest 修复 + 前端单测(解锁 `jlpm test` 工作流)
|
||||||
|
4. CI 加 pytest
|
||||||
|
5. settings schema / restore 端点(按需)
|
||||||
|
6. ui-tests(锦上添花)
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
"@jupyterlab/notebook": "^4.0.0",
|
"@jupyterlab/notebook": "^4.0.0",
|
||||||
"@jupyterlab/services": "^7.0.0",
|
"@jupyterlab/services": "^7.0.0",
|
||||||
"@jupyterlab/settingregistry": "^4.0.0",
|
"@jupyterlab/settingregistry": "^4.0.0",
|
||||||
|
"@jupyterlab/ui-components": "^4.0.0",
|
||||||
"@lumino/signaling": "^2.0.0",
|
"@lumino/signaling": "^2.0.0",
|
||||||
"@lumino/widgets": "^2.0.0",
|
"@lumino/widgets": "^2.0.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
|
|||||||
+9
-3
@@ -5,6 +5,7 @@ import {
|
|||||||
import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
|
import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
|
||||||
import { ToolbarButton, MainAreaWidget } from '@jupyterlab/apputils';
|
import { ToolbarButton, MainAreaWidget } from '@jupyterlab/apputils';
|
||||||
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
||||||
|
import { saveIcon } from '@jupyterlab/ui-components';
|
||||||
|
|
||||||
import { requestAPI } from './request';
|
import { requestAPI } from './request';
|
||||||
import { registerCommands, CommandIDs } from './commands';
|
import { registerCommands, CommandIDs } from './commands';
|
||||||
@@ -59,7 +60,11 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|||||||
onOpenDiff: openDiff
|
onOpenDiff: openDiff
|
||||||
});
|
});
|
||||||
panel.id = 'snapshot:panel';
|
panel.id = 'snapshot:panel';
|
||||||
panel.title.label = '快照历史';
|
// Icon-only panel tab: the title text becomes a hover caption so the
|
||||||
|
// sidebar stays compact. saveIcon is reused for visual consistency with
|
||||||
|
// the commit toolbar button (both signal "save" semantics).
|
||||||
|
panel.title.icon = saveIcon;
|
||||||
|
panel.title.caption = '快照历史';
|
||||||
app.shell.add(panel, 'left', { rank: 200 });
|
app.shell.add(panel, 'left', { rank: 200 });
|
||||||
|
|
||||||
// Keep the model in sync with the current notebook.
|
// Keep the model in sync with the current notebook.
|
||||||
@@ -73,13 +78,14 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|||||||
|
|
||||||
registerCommands(app, tracker, model);
|
registerCommands(app, tracker, model);
|
||||||
|
|
||||||
// Add a "保存快照" button to every notebook panel's toolbar.
|
// Add an icon-only "保存快照" button to every notebook panel's toolbar.
|
||||||
|
// The tooltip on hover carries the full description.
|
||||||
// NOTE: docRegistry.addWidgetExtension('Notebook', ...) places its
|
// NOTE: docRegistry.addWidgetExtension('Notebook', ...) places its
|
||||||
// returned widget in the *main area* (a new tab), not on the toolbar.
|
// returned widget in the *main area* (a new tab), not on the toolbar.
|
||||||
// The toolbar wants panel.toolbar.addItem directly.
|
// The toolbar wants panel.toolbar.addItem directly.
|
||||||
const addCommitButton = (panelHost: NotebookPanel) => {
|
const addCommitButton = (panelHost: NotebookPanel) => {
|
||||||
const button = new ToolbarButton({
|
const button = new ToolbarButton({
|
||||||
label: '保存快照',
|
icon: saveIcon,
|
||||||
tooltip: '保存当前 notebook 为快照',
|
tooltip: '保存当前 notebook 为快照',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
void app.commands.execute(CommandIDs.commit);
|
void app.commands.execute(CommandIDs.commit);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ReactWidget } from '@jupyterlab/apputils';
|
import { ReactWidget } from '@jupyterlab/apputils';
|
||||||
import { ServerConnection } from '@jupyterlab/services';
|
import { ServerConnection } from '@jupyterlab/services';
|
||||||
|
import { refreshIcon } from '@jupyterlab/ui-components';
|
||||||
|
|
||||||
import { getSnapshotContent } from '../api';
|
import { getSnapshotContent } from '../api';
|
||||||
import { INotebook } from '../diff';
|
import { INotebook } from '../diff';
|
||||||
@@ -50,14 +51,18 @@ const Panel: React.FC<PanelProps> = ({ model, app, tracker, serverSettings, onOp
|
|||||||
<div className="jp-snapshot-panel-container">
|
<div className="jp-snapshot-panel-container">
|
||||||
<div className="jp-snapshot-panel-toolbar">
|
<div className="jp-snapshot-panel-toolbar">
|
||||||
<button
|
<button
|
||||||
className="jp-snapshot-panel-refresh-button"
|
className={
|
||||||
|
'jp-snapshot-panel-refresh-button' +
|
||||||
|
(model.loading ? ' is-loading' : '')
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void model.refresh();
|
void model.refresh();
|
||||||
}}
|
}}
|
||||||
disabled={model.loading}
|
disabled={model.loading}
|
||||||
title="重新拉取版本列表"
|
title="重新拉取版本列表"
|
||||||
|
aria-label="刷新"
|
||||||
>
|
>
|
||||||
{model.loading ? '刷新中…' : '刷新'}
|
<refreshIcon.react className="jp-Icon jp-Icon-16" tag="span" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{model.loading && model.versions.length === 0 ? (
|
{model.loading && model.versions.length === 0 ? (
|
||||||
|
|||||||
+21
-2
@@ -26,13 +26,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.jp-snapshot-panel-refresh-button {
|
.jp-snapshot-panel-refresh-button {
|
||||||
font-size: 0.8em;
|
display: inline-flex;
|
||||||
padding: 2px 8px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
border: 1px solid var(--jp-border-color2, #ccc);
|
border: 1px solid var(--jp-border-color2, #ccc);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: var(--jp-layout-color2, #f5f5f5);
|
background: var(--jp-layout-color2, #f5f5f5);
|
||||||
color: var(--jp-ui-font-color1, #333);
|
color: var(--jp-ui-font-color1, #333);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jp-snapshot-panel-refresh-button:hover:not(:disabled) {
|
.jp-snapshot-panel-refresh-button:hover:not(:disabled) {
|
||||||
@@ -44,6 +49,20 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jp-snapshot-panel-refresh-button.is-loading svg,
|
||||||
|
.jp-snapshot-panel-refresh-button.is-loading .jp-Icon {
|
||||||
|
animation: jp-snapshot-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes jp-snapshot-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.jp-snapshot-panel-empty {
|
.jp-snapshot-panel-empty {
|
||||||
color: var(--jp-ui-font-color2, #555);
|
color: var(--jp-ui-font-color2, #555);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
@@ -1969,7 +1969,6 @@ wheels = [
|
|||||||
name = "snapshot"
|
name = "snapshot"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "boto3" },
|
|
||||||
{ name = "jupyter-server" },
|
{ name = "jupyter-server" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1978,6 +1977,9 @@ dev = [
|
|||||||
{ name = "jupyter-builder" },
|
{ name = "jupyter-builder" },
|
||||||
{ name = "jupyterlab" },
|
{ name = "jupyterlab" },
|
||||||
]
|
]
|
||||||
|
s3 = [
|
||||||
|
{ name = "boto3" },
|
||||||
|
]
|
||||||
test = [
|
test = [
|
||||||
{ name = "coverage" },
|
{ name = "coverage" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
@@ -1988,7 +1990,7 @@ test = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "boto3", specifier = ">=1.43.52" },
|
{ name = "boto3", marker = "extra == 's3'", specifier = ">=1.34.0" },
|
||||||
{ name = "coverage", marker = "extra == 'test'" },
|
{ name = "coverage", marker = "extra == 'test'" },
|
||||||
{ name = "jupyter-builder", marker = "extra == 'dev'", specifier = ">=1.0.0" },
|
{ name = "jupyter-builder", marker = "extra == 'dev'", specifier = ">=1.0.0" },
|
||||||
{ name = "jupyter-server", specifier = ">=2.4.0,<3" },
|
{ name = "jupyter-server", specifier = ">=2.4.0,<3" },
|
||||||
@@ -1998,7 +2000,7 @@ requires-dist = [
|
|||||||
{ name = "pytest-cov", marker = "extra == 'test'" },
|
{ name = "pytest-cov", marker = "extra == 'test'" },
|
||||||
{ name = "pytest-jupyter", extras = ["server"], marker = "extra == 'test'", specifier = ">=0.6.0" },
|
{ name = "pytest-jupyter", extras = ["server"], marker = "extra == 'test'", specifier = ">=0.6.0" },
|
||||||
]
|
]
|
||||||
provides-extras = ["dev", "test"]
|
provides-extras = ["dev", "s3", "test"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "soupsieve"
|
name = "soupsieve"
|
||||||
|
|||||||
@@ -2785,7 +2785,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@jupyterlab/ui-components@npm:^4.6.1":
|
"@jupyterlab/ui-components@npm:^4.0.0, @jupyterlab/ui-components@npm:^4.6.1":
|
||||||
version: 4.6.1
|
version: 4.6.1
|
||||||
resolution: "@jupyterlab/ui-components@npm:4.6.1"
|
resolution: "@jupyterlab/ui-components@npm:4.6.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9222,6 +9222,7 @@ __metadata:
|
|||||||
"@jupyterlab/settingregistry": ^4.0.0
|
"@jupyterlab/settingregistry": ^4.0.0
|
||||||
"@jupyterlab/testing": ^4.6.1
|
"@jupyterlab/testing": ^4.6.1
|
||||||
"@jupyterlab/testutils": ^4.0.0
|
"@jupyterlab/testutils": ^4.0.0
|
||||||
|
"@jupyterlab/ui-components": ^4.0.0
|
||||||
"@lumino/signaling": ^2.0.0
|
"@lumino/signaling": ^2.0.0
|
||||||
"@lumino/widgets": ^2.0.0
|
"@lumino/widgets": ^2.0.0
|
||||||
"@module-federation/runtime-tools": ^2.0.0
|
"@module-federation/runtime-tools": ^2.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user