From b78760ff4b452f2589fa9859fb31da5a957605f4 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 21 Oct 2026 05:03:46 +0900 Subject: [PATCH 1/2] Use course manifest pins for installation and updates --- .github/workflows/fresh-install.yml | 12 ++++++------ .github/workflows/online-ebook.yml | 7 ++----- README.md | 16 +++++++++------- README.zh.hant.md | 16 +++++++++------- README.zh.md | 16 +++++++++------- book/en/src/base/chapter_1.md | 24 +++++------------------- book/en/src/changelog.md | 7 +++++++ book/src/base/chapter_1.md | 24 +++++------------------- book/src/changelog.md | 7 +++++++ docs/environment.md | 20 ++++++++++++++++++-- docs/environment.zh.md | 18 +++++++++++++++++- scripts/ci/latest.py | 10 ++++++++-- scripts/ci/readme.py | 26 +++++++++++++++++++++----- scripts/ci/versions.py | 4 ++-- 14 files changed, 125 insertions(+), 82 deletions(-) diff --git a/.github/workflows/fresh-install.yml b/.github/workflows/fresh-install.yml index 5b7a90f..cedd1c8 100644 --- a/.github/workflows/fresh-install.yml +++ b/.github/workflows/fresh-install.yml @@ -218,14 +218,14 @@ jobs: include-hidden-files: true released-course: - name: Published d2x install route (main only) - if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && inputs.candidate_run == '' + name: d2x download and pinned install + if: ${{ !inputs.diagnose && inputs.candidate_run == '' }} runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - uses: actions/checkout@v4 - - name: Follow the README using the public course download - run: python3 scripts/ci/readme.py --published + - name: Verify the documented existing-d2x download route + run: python3 scripts/ci/readme.py --d2x-download ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && '--published' || '' }} - uses: actions/upload-artifact@v4 if: always() with: @@ -265,7 +265,7 @@ jobs: include-hidden-files: true latest-stable: - if: github.ref == 'refs/heads/main' && inputs.candidate_run == '' + if: ${{ !inputs.diagnose && inputs.candidate_run == '' }} runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -280,7 +280,7 @@ jobs: if: runner.os == 'Windows' shell: powershell run: ./scripts/install-xlings.ps1 - - name: Check stable release, public latest and course pin + - name: Check public latest and independently restore course pins shell: bash env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/online-ebook.yml b/.github/workflows/online-ebook.yml index 4f80503..aa40354 100644 --- a/.github/workflows/online-ebook.yml +++ b/.github/workflows/online-ebook.yml @@ -45,9 +45,6 @@ jobs: run: working-directory: book - env: - MDBOOK_VERSION: 0.4.43 - steps: - uses: actions/checkout@v4 @@ -57,8 +54,8 @@ jobs: - name: Install the shared xlings baseline run: bash ../scripts/install-xlings.sh - - name: Install mdBook - run: xlings install mdbook@${MDBOOK_VERSION} -y + - name: Install the course dependencies from .xlings.json + run: xlings install -y - name: Setup Pages if: github.event_name != 'pull_request' diff --git a/README.md b/README.md index 9849577..fef5434 100644 --- a/README.md +++ b/README.md @@ -43,22 +43,24 @@ Wait for the environment to be ready, then run `d2x checker` in the terminal. ### Practice locally -**1. Install the tools** +**1. Get the course** -Install [xlings](docs/environment.md#install-xlings), reopen your terminal, then run: +Install [xlings](docs/environment.md#install-xlings) and Git, then reopen your terminal. ```bash -xlings update -xlings install --use d2x@latest mcpp@latest -y +git clone https://github.com/mcpp-community/d2mcpp.git +cd d2mcpp ``` -**2. Get the course** +**2. Install the course versions** ```bash -d2x install d2mcpp -cd d2mcpp +xlings update +xlings install -y ``` +Run these commands from the course directory. `xlings install` installs the versions declared in `.xlings.json`; after updating the course with Git, run the same two commands again. + **3. Verify and practice** ```bash diff --git a/README.zh.hant.md b/README.zh.hant.md index d9ff7cd..a5b5168 100644 --- a/README.zh.hant.md +++ b/README.zh.hant.md @@ -43,22 +43,24 @@ ### 建立本地練習環境 -**第一步:安裝工具** +**第一步:取得課程** -安裝 [xlings](docs/environment.zh.md#install-xlings),重新開啟終端後執行: +安裝 [xlings](docs/environment.zh.md#install-xlings) 和 Git,然後重新開啟終端。 ```bash -xlings update -xlings install --use d2x@latest mcpp@latest -y +git clone https://github.com/mcpp-community/d2mcpp.git +cd d2mcpp ``` -**第二步:取得課程** +**第二步:安裝課程版本** ```bash -d2x install d2mcpp -cd d2mcpp +xlings update +xlings install -y ``` +在課程目錄執行上述命令。`xlings install` 安裝 `.xlings.json` 宣告的版本;透過 Git 更新課程後,再次執行這兩條命令。 + **第三步:驗證並開始練習** ```bash diff --git a/README.zh.md b/README.zh.md index 0d104b6..5e852bb 100644 --- a/README.zh.md +++ b/README.zh.md @@ -43,22 +43,24 @@ ### 搭建本地练习环境 -**第一步:安装工具** +**第一步:获取课程** -安装 [xlings](docs/environment.zh.md#install-xlings),重新打开终端后执行: +安装 [xlings](docs/environment.zh.md#install-xlings) 和 Git,然后重新打开终端。 ```bash -xlings update -xlings install --use d2x@latest mcpp@latest -y +git clone https://github.com/mcpp-community/d2mcpp.git +cd d2mcpp ``` -**第二步:获取课程** +**第二步:安装课程版本** ```bash -d2x install d2mcpp -cd d2mcpp +xlings update +xlings install -y ``` +在课程目录执行上述命令。`xlings install` 安装 `.xlings.json` 声明的版本;通过 Git 更新课程后,再次执行这两条命令。 + **第三步:验证并开始练习** ```bash diff --git a/book/en/src/base/chapter_1.md b/book/en/src/base/chapter_1.md index caf0040..f02c3c7 100644 --- a/book/en/src/base/chapter_1.md +++ b/book/en/src/base/chapter_1.md @@ -40,32 +40,18 @@ $env:Path = "$env:USERPROFILE\.xlings\subos\current\bin;$env:Path" The PATH command makes the installed tools available in this terminal. For future sessions, reopen your terminal after installation. Preparation can be repeated and preserves your exercise files and progress.
-Alternative: download with d2x install +Alternative: download with an existing d2x -Install [xlings](https://github.com/openxlings/xlings) first: +If d2x is already installed, use the commands below. After downloading, run `xlings install -y` from the course directory to install the `.xlings.json` dependencies. There is no need to preinstall mcpp or select latest. ```bash -curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/v2026.10.18.1/tools/other/quick_install.sh | bash -s v2026.10.18.1 -export PATH="$HOME/.xlings/subos/current/bin:$PATH" -``` - -Or on Windows PowerShell: - -```powershell -irm https://raw.githubusercontent.com/mcpp-community/d2mcpp/main/scripts/install-xlings.ps1 | iex -$env:Path = "$env:USERPROFILE\.xlings\subos\current\bin;$env:Path" -``` - -Then, on either system: - -```text -xlings update -xlings install --use d2x@latest mcpp@latest -y d2x install d2mcpp cd d2mcpp +xlings update +xlings install -y ``` -Continue with the preparation script for your system in step 2; both download routes use the same verification below. +Then continue with the preparation script in step 2. Apply project pins again after updating the course.
diff --git a/book/en/src/changelog.md b/book/en/src/changelog.md index 847d4bb..23467bc 100644 --- a/book/en/src/changelog.md +++ b/book/en/src/changelog.md @@ -10,6 +10,13 @@ ## 2026/10 +**Install dependencies from the course manifest** + +Get the course before running `xlings install -y` to install the versions pinned in `.xlings.json`. Align update instructions and verify both Git and d2x downloads in CI. Check upstream latest independently of course dependency upgrades. + +- `Book`: [zh](https://mcpp-community.github.io/d2mcpp/base/chapter_1.html) / [en](https://mcpp-community.github.io/d2mcpp/en/base/chapter_1.html) - 2026/10/21 +- `Code`: [first-use CI](https://github.com/mcpp-community/d2mcpp/blob/main/.github/workflows/fresh-install.yml) - 2026/10/21 + **d2x upgrade and cold-start diagnostics** Upgrade d2x to 2026.10.21.1 and verify the selected executable; bound first Provider startup separately, show preparation and retain raw logs. diff --git a/book/src/base/chapter_1.md b/book/src/base/chapter_1.md index 8cb37f0..e27412c 100644 --- a/book/src/base/chapter_1.md +++ b/book/src/base/chapter_1.md @@ -40,32 +40,18 @@ $env:Path = "$env:USERPROFILE\.xlings\subos\current\bin;$env:Path" PATH 命令让当前终端立即找到工具;安装后重新打开终端即可供后续会话使用。准备脚本可重复执行,不会改动练习文件或学习进度。
-可选:通过 d2x install 下载课程 +可选:通过已有 d2x 下载课程 -先安装 [xlings](https://github.com/openxlings/xlings): +如果已经安装 d2x,可以使用以下方式。下载后在课程目录执行 `xlings install -y`,按 `.xlings.json` 安装依赖,无需提前安装 mcpp 或选择 latest。 ```bash -curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/v2026.10.18.1/tools/other/quick_install.sh | bash -s v2026.10.18.1 -export PATH="$HOME/.xlings/subos/current/bin:$PATH" -``` - -Windows PowerShell 使用: - -```powershell -irm https://raw.githubusercontent.com/mcpp-community/d2mcpp/main/scripts/install-xlings.ps1 | iex -$env:Path = "$env:USERPROFILE\.xlings\subos\current\bin;$env:Path" -``` - -然后在任一系统执行: - -```text -xlings update -xlings install --use d2x@latest mcpp@latest -y d2x install d2mcpp cd d2mcpp +xlings update +xlings install -y ``` -继续执行第二步中对应系统的准备脚本;两种下载方式共用下面的验证步骤。 +之后继续第二步的准备脚本。更新课程后同样按项目配置安装依赖。
diff --git a/book/src/changelog.md b/book/src/changelog.md index 3d2977d..2ae3dd4 100644 --- a/book/src/changelog.md +++ b/book/src/changelog.md @@ -10,6 +10,13 @@ ## 2026/10 +**按课程配置安装依赖** + +默认先获取课程,再由 `xlings install -y` 安装 `.xlings.json` 固定的版本;统一更新说明,并在 CI 验证 Git 与 d2x 下载路径。上游 latest 检查独立于课程版本升级。 + +- `Book`: [zh](https://mcpp-community.github.io/d2mcpp/base/chapter_1.html) / [en](https://mcpp-community.github.io/d2mcpp/en/base/chapter_1.html) - 2026/10/21 +- `Code`: [first-use CI](https://github.com/mcpp-community/d2mcpp/blob/main/.github/workflows/fresh-install.yml) - 2026/10/21 + **d2x 最新版联动与冷启动诊断** 升级 d2x 至 2026.10.21.1,核对实际执行版本;首次 Provider 构建单独限时,显示准备进度并保留原始日志。 diff --git a/docs/environment.md b/docs/environment.md index ff8c5ca..0afde33 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -26,7 +26,7 @@ Continue with the README. If a command is not found, reopen your terminal or app ## Git installation and updates -After installing xlings, you can also clone the course. Replace the URL with your fork if needed: +After installing xlings, clone the course as shown in the README. Replace the URL with your fork if needed: ```bash git clone https://github.com/mcpp-community/d2mcpp.git @@ -104,6 +104,22 @@ limit. Preparation messages do not extend the startup deadline. On failure, keep `.d2x/provider.stdout.log`, `.d2x/provider.stderr.log` and the command output. Startup/transport errors exit nonzero; an unfinished exercise verdict is expected. -For an existing tool installation, use `xlings install --use d2x@latest mcpp@latest -y` after `xlings update`. `@latest` selects the newest stable index entry; `--use` activates it even when already installed. Bare install commands can retain the previously selected version. +From an existing course directory, install or restore the versions declared in `.xlings.json` with the commands below. `xlings update` refreshes the package index; `xlings install -y` installs the project dependencies. Use the course pins rather than `@latest`. Dependency upgrades belong in `.xlings.json` and must pass CI before publication. + +```bash +xlings update +xlings install -y +``` User cancellation returns 130 and a `cancelled` event without a Provider-failure warning or a failed exercise verdict. + +## Download with an existing d2x + +If d2x is already available, you can use this download route. The installer applies the downloaded course dependencies; running `xlings install -y` from the course directory explicitly reapplies its pins. There is no need to preinstall mcpp or select latest. + +```bash +d2x install d2mcpp +cd d2mcpp +xlings update +xlings install -y +``` diff --git a/docs/environment.zh.md b/docs/environment.zh.md index d70c175..e5e8bfa 100644 --- a/docs/environment.zh.md +++ b/docs/environment.zh.md @@ -100,6 +100,22 @@ checker 在第一题之前显示准备信息。首次 Provider 启动的总期 准备提示不会延长期限。失败时保留 `.d2x/provider.stdout.log`、`.d2x/provider.stderr.log` 与命令输出。 启动和传输错误会非零退出;未完成练习的 fail 判定属于预期行为。 -已有工具环境先执行 `xlings update`,再执行 `xlings install --use d2x@latest mcpp@latest -y`。`@latest` 选择最新稳定条目;`--use` 即使新版已安装也会激活它。仅执行不带版本或激活选项的 install 可能继续保留旧版。 +在已有课程目录中,执行以下命令安装或恢复 `.xlings.json` 声明的版本。`xlings update` 刷新包索引;`xlings install -y` 按项目配置安装依赖。不要用 `@latest` 替代课程版本。升级依赖应先修改 `.xlings.json`,通过 CI 后发布。 + +```bash +xlings update +xlings install -y +``` 用户取消返回 130 和 `cancelled` 事件,不报告 Provider 故障,也不生成练习失败判定。 + +## 通过已有 d2x 下载课程 + +如果已经安装 d2x,可以使用以下下载方式。安装器会应用下载课程中的依赖配置;进入课程目录后再次运行 `xlings install -y` 可明确核对安装。无需提前安装 mcpp 或切换到 latest。 + +```bash +d2x install d2mcpp +cd d2mcpp +xlings update +xlings install -y +``` diff --git a/scripts/ci/latest.py b/scripts/ci/latest.py index d967524..37f2699 100644 --- a/scripts/ci/latest.py +++ b/scripts/ci/latest.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Compare the stable d2x release, course pin and real public latest install.""" +"""Verify public latest independently, then verify the reviewed project pins.""" import json import os from pathlib import Path @@ -7,6 +7,8 @@ import tempfile import urllib.request +from versions import verify + root = Path(__file__).resolve().parents[2] logs = root / '.d2x/ci-logs' logs.mkdir(parents=True, exist_ok=True) @@ -29,4 +31,8 @@ (logs / 'latest.json').write_text(json.dumps(report, indent=2)) print(json.dumps(report, indent=2), flush=True) assert actual == latest, 'Public index/install is behind the stable release' -assert pin == latest, 'Bump the course pin and run the full first-use matrix for the new stable release' +# A new upstream release must not force an unreviewed course dependency bump. +manifest = (root / '.xlings.json').read_bytes() +subprocess.run(['xlings', 'install', '-y'], cwd=root, check=True) +verify(label='pinned-after-latest') +assert (root / '.xlings.json').read_bytes() == manifest, 'Install changed the reviewed manifest' diff --git a/scripts/ci/readme.py b/scripts/ci/readme.py index 4a1b4c4..6d26a1e 100644 --- a/scripts/ci/readme.py +++ b/scripts/ci/readme.py @@ -43,8 +43,23 @@ def readme_commands(name, start, end): install = re.search(r'\s*##[^\n]*\n(.*?)(?=\n## |\Z)', guide, re.S)[1] bootstrap = code_blocks(install, 'powershell' if os.name == 'nt' else 'bash')[0] parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--d2x-download', action='store_true', help='verify the optional existing-d2x download route') parser.add_argument('--published', action='store_true', help='download the public course instead of the candidate fixture') args = parser.parse_args() +if args.d2x_download: + # Only this optional route needs d2x before the course exists. Bootstrap the + # reviewed course version in a neutral directory; mcpp comes from its manifest. + pins = json.loads((ROOT / '.xlings.json').read_text())['workspace'] + commands = code_blocks(guide.split('## Download with an existing d2x', 1)[1], 'bash')[0].splitlines() + assert commands == code_blocks(translation.split('## 通过已有 d2x 下载课程', 1)[1], 'bash')[0].splitlines() + commands += ['mcpp test -p solutions', 'd2x checker'] + bootstrap += '\nxlings update\nxlings install --use d2x@' + pins['d2x'] + ' -y\n' + if os.name == 'nt': + bootstrap += "if ($LASTEXITCODE -ne 0) { throw 'd2x bootstrap failed' }\n" +assert commands[-2:] == ['mcpp test -p solutions', 'd2x checker'] +assert 'xlings install -y' in commands +assert not any('@latest' in command for command in commands) + assert not WORK.exists(), 'Use a fresh disposable checkout' assert not (ROOT / '.d2x/state.json').exists(), 'Use a checkout without learning progress' @@ -80,14 +95,14 @@ def readme_commands(name, start, end): if command == 'd2x install d2mcpp' and os.environ.get('D2X_CANDIDATE'): command = "& '" + os.environ['D2X_CANDIDATE'].replace("'", "''") + "' install d2mcpp" body += command + "\nif ($LASTEXITCODE -ne 0) { throw 'README command failed' }\n" - if command == 'xlings install --use d2x@latest mcpp@latest -y': - body += f"python '{ROOT / 'scripts/ci/versions.py'}' --public '{LOGS / 'public-tool-versions.json'}'\nif ($LASTEXITCODE -ne 0) {{ throw 'Public version snapshot failed' }}\n" + if command == 'xlings install -y': + body += f"python '{ROOT / 'scripts/ci/versions.py'}' --public '{LOGS / 'installed-tool-versions.json'}'\nif ($LASTEXITCODE -ne 0) {{ throw 'Installed version snapshot failed' }}\n" script.write_text(body, encoding='utf-8') shell = ['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', str(script)] else: import shlex - offset = commands.index('xlings install --use d2x@latest mcpp@latest -y') + 1 - commands.insert(offset, 'python3 ' + shlex.quote(str(ROOT / 'scripts/ci/versions.py')) + ' --public ' + shlex.quote(str(LOGS / 'public-tool-versions.json'))) + offset = commands.index('xlings install -y') + 1 + commands.insert(offset, 'python3 ' + shlex.quote(str(ROOT / 'scripts/ci/versions.py')) + ' --public ' + shlex.quote(str(LOGS / 'installed-tool-versions.json'))) if os.environ.get('D2X_CANDIDATE'): import shlex commands = [shlex.quote(os.environ['D2X_CANDIDATE']) + ' install d2mcpp' if c == 'd2x install d2mcpp' else c for c in commands] @@ -110,11 +125,12 @@ def readme_commands(name, start, end): assert expected > 0 and re.findall(r'(\d+) passed; (\d+) failed', output)[-1] == (str(expected), '0') assert f'FIRST-RUN PASS: {expected} exercises' in output actual = subprocess.check_output(['git', '-C', str(course), 'rev-parse', 'HEAD'], text=True).strip() + subprocess.run(['git', '-C', str(course), 'diff', '--exit-code', '--', '.xlings.json'], check=True) if args.published: assert json.loads((course / '.xlings.json').read_text()) == json.loads((ROOT / '.xlings.json').read_text()), 'Published course pins are stale' else: assert actual == head, f'Course download used {actual}, expected candidate {head}' - (LOGS / 'readme-result.json').write_text(json.dumps({'candidate': head, 'downloaded': actual, 'published': args.published, 'status': 'passed'}, indent=2), encoding='utf-8') + (LOGS / 'readme-result.json').write_text(json.dumps({'candidate': head, 'downloaded': actual, 'published': args.published, 'route': 'd2x' if args.d2x_download else 'git', 'status': 'passed'}, indent=2), encoding='utf-8') finally: if (course / '.d2x/ci-logs').exists(): shutil.copytree(course / '.d2x/ci-logs', LOGS / 'readme-events', dirs_exist_ok=True) diff --git a/scripts/ci/versions.py b/scripts/ci/versions.py index 7dad16b..530a5f2 100644 --- a/scripts/ci/versions.py +++ b/scripts/ci/versions.py @@ -17,11 +17,11 @@ def checker_binary(): def verify(root=ROOT, label='versions'): pins = json.loads((root / '.xlings.json').read_text())['workspace'] result = {'pins': pins, 'cwd': str(root), 'tools': {}} - for name in ['d2x', 'mcpp']: + for name in pins: actual = subprocess.check_output([name, '--version'], cwd=root, text=True, encoding='utf-8').strip() path = shutil.which(name) result['tools'][name] = {'version': actual, 'path': path} - assert actual.split()[-1] == pins[name], f'{name}: actual {actual}; expected {pins[name]}; path {path}' + assert actual.split()[-1].removeprefix('v') == pins[name], f'{name}: actual {actual}; expected {pins[name]}; path {path}' if os.environ.get('D2X_CANDIDATE'): binary = Path(os.environ['D2X_CANDIDATE']) provenance = json.loads(binary.with_name('provenance.json').read_text()) From 7a4a36f232d7405a5a95cb37237bc6ec5e2e1642 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 21 Oct 2026 05:05:16 +0900 Subject: [PATCH 2/2] Verify older project pins remain isolated from public latest --- scripts/ci/latest.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/ci/latest.py b/scripts/ci/latest.py index 37f2699..d04868b 100644 --- a/scripts/ci/latest.py +++ b/scripts/ci/latest.py @@ -27,7 +27,19 @@ assert old == '2026.08.02.2', 'Old-version activation fixture failed' subprocess.run(['xlings', 'install', '--use', 'd2x@latest', '-y'], cwd=neutral, check=True) actual = subprocess.check_output(['d2x', '--version'], cwd=neutral, text=True).strip() -report = {'previous': old, 'latest': latest, 'course_pin': pin, 'public_install': actual, 'release': release['html_url']} + # Deliberately keep a project behind latest to exercise version isolation, + # even when the real course pin currently equals the latest release. + project = Path(neutral) / 'older-course' + project.mkdir() + fixture = json.dumps({'workspace': {'d2x': old}}) + '\n' + (project / '.xlings.json').write_text(fixture) + subprocess.run(['xlings', 'install', '-y'], cwd=project, check=True) + selected = subprocess.check_output(['d2x', '--version'], cwd=project, text=True).strip() + assert selected == old, f'Project pin ignored: {selected}; expected {old}' + assert (project / '.xlings.json').read_text() == fixture, 'Install rewrote the fixture pin' + outside = subprocess.check_output(['d2x', '--version'], cwd=neutral, text=True).strip() + assert outside == actual, 'Project install changed the global selection' +report = {'previous': old, 'latest': latest, 'course_pin': pin, 'public_install': actual, 'release': release['html_url'], 'older_project_selection': selected, 'global_after_project_install': outside} (logs / 'latest.json').write_text(json.dumps(report, indent=2)) print(json.dumps(report, indent=2), flush=True) assert actual == latest, 'Public index/install is behind the stable release'