From d99538843d83d6422e0b1e9753e6cab44b55c063 Mon Sep 17 00:00:00 2001 From: Open-Squilla <275096992+Open-Squilla@users.noreply.github.com> Date: Fri, 21 Aug 2026 06:57:56 +0800 Subject: [PATCH] Prepare native Runtime Pack publication --- .github/workflows/build-release.yml | 30 +++---- .github/workflows/mirror-oss.yml | 120 ++++++++++++++++++++++++---- README.md | 47 +++++++---- SECURITY.md | 27 +++++-- scripts/build_pack.py | 73 ++++++++++++++--- scripts/validate_sources.py | 14 +++- sources.json | 102 +++++++++++------------ tests/test_pack_safety.py | 63 +++++++++++++++ tests/test_validate_sources.py | 6 +- tests/test_workflow_contract.py | 18 +++-- 10 files changed, 376 insertions(+), 124 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 42d4efe..45ec505 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: releaseTag: - description: Exact reviewed tag from sources.json (for example v2026.07.30.1) + description: Exact reviewed tag from sources.json (for example v2026.08.21.1) required: true createDraft: description: Create the protected GitHub Draft Release after all audits pass @@ -72,20 +72,20 @@ jobs: fail-fast: false matrix: include: - - { target: darwin-arm64, component: python, runner: '["self-hosted","opensquilla-runtime","darwin-arm64"]' } - - { target: darwin-arm64, component: node, runner: '["self-hosted","opensquilla-runtime","darwin-arm64"]' } - - { target: darwin-x64, component: python, runner: '["self-hosted","opensquilla-runtime","darwin-x64"]' } - - { target: darwin-x64, component: node, runner: '["self-hosted","opensquilla-runtime","darwin-x64"]' } - - { target: linux-arm64, component: python, runner: '["self-hosted","opensquilla-runtime","linux-arm64","glibc"]' } - - { target: linux-arm64, component: node, runner: '["self-hosted","opensquilla-runtime","linux-arm64","glibc"]' } - - { target: linux-x64, component: python, runner: '["self-hosted","opensquilla-runtime","linux-x64","glibc"]' } - - { target: linux-x64, component: node, runner: '["self-hosted","opensquilla-runtime","linux-x64","glibc"]' } - - { target: windows-arm64, component: python, runner: '["self-hosted","opensquilla-runtime","windows-arm64"]' } - - { target: windows-arm64, component: node, runner: '["self-hosted","opensquilla-runtime","windows-arm64"]' } - - { target: windows-arm64, component: gitBash, runner: '["self-hosted","opensquilla-runtime","windows-arm64"]' } - - { target: windows-x64, component: python, runner: '["self-hosted","opensquilla-runtime","windows-x64"]' } - - { target: windows-x64, component: node, runner: '["self-hosted","opensquilla-runtime","windows-x64"]' } - - { target: windows-x64, component: gitBash, runner: '["self-hosted","opensquilla-runtime","windows-x64"]' } + - { target: darwin-arm64, component: python, runner: '["macos-15"]' } + - { target: darwin-arm64, component: node, runner: '["macos-15"]' } + - { target: darwin-x64, component: python, runner: '["macos-15-intel"]' } + - { target: darwin-x64, component: node, runner: '["macos-15-intel"]' } + - { target: linux-arm64, component: python, runner: '["ubuntu-24.04-arm"]' } + - { target: linux-arm64, component: node, runner: '["ubuntu-24.04-arm"]' } + - { target: linux-x64, component: python, runner: '["ubuntu-24.04"]' } + - { target: linux-x64, component: node, runner: '["ubuntu-24.04"]' } + - { target: windows-arm64, component: python, runner: '["windows-11-arm"]' } + - { target: windows-arm64, component: node, runner: '["windows-11-arm"]' } + - { target: windows-arm64, component: gitBash, runner: '["windows-11-arm"]' } + - { target: windows-x64, component: python, runner: '["windows-2025"]' } + - { target: windows-x64, component: node, runner: '["windows-2025"]' } + - { target: windows-x64, component: gitBash, runner: '["windows-2025"]' } runs-on: ${{ fromJSON(matrix.runner) }} timeout-minutes: 90 steps: diff --git a/.github/workflows/mirror-oss.yml b/.github/workflows/mirror-oss.yml index c26abfb..a4080a7 100644 --- a/.github/workflows/mirror-oss.yml +++ b/.github/workflows/mirror-oss.yml @@ -27,11 +27,11 @@ jobs: timeout-minutes: 180 environment: runtime-pack-oss env: - OSS_BUCKET: ${{ vars.RUNTIME_PACK_OSS_BUCKET }} - OSS_ACCESS_KEY_ID: ${{ secrets.RUNTIME_PACK_OSS_ACCESS_KEY_ID }} - OSS_ACCESS_KEY_SECRET: ${{ secrets.RUNTIME_PACK_OSS_ACCESS_KEY_SECRET }} + OSS_BUCKET: opensquilla-releases + OSS_ACCESS_KEY_ID: ${{ secrets.ALIYUN_OSS_ACCESS_KEY_ID }} + OSS_ACCESS_KEY_SECRET: ${{ secrets.ALIYUN_OSS_ACCESS_KEY_SECRET }} OSS_REGION: cn-beijing - OSS_ENDPOINT: ${{ vars.RUNTIME_PACK_OSS_ENDPOINT }} + OSS_ENDPOINT: https://oss-cn-beijing.aliyuncs.com steps: - name: Resolve and validate published tag id: release @@ -88,9 +88,8 @@ jobs: shell: bash run: | set -euo pipefail - [[ -n "${OSS_BUCKET}" ]] || { echo 'Missing RUNTIME_PACK_OSS_BUCKET' >&2; exit 1; } - [[ -n "${OSS_ACCESS_KEY_ID}" ]] || { echo 'Missing RUNTIME_PACK_OSS_ACCESS_KEY_ID' >&2; exit 1; } - [[ -n "${OSS_ACCESS_KEY_SECRET}" ]] || { echo 'Missing RUNTIME_PACK_OSS_ACCESS_KEY_SECRET' >&2; exit 1; } + [[ -n "${OSS_ACCESS_KEY_ID}" ]] || { echo 'Missing ALIYUN_OSS_ACCESS_KEY_ID' >&2; exit 1; } + [[ -n "${OSS_ACCESS_KEY_SECRET}" ]] || { echo 'Missing ALIYUN_OSS_ACCESS_KEY_SECRET' >&2; exit 1; } [[ "${OSS_BUCKET}" == "opensquilla-releases" ]] || { echo 'Runtime Packs must mirror to the reviewed opensquilla-releases bucket.' >&2 exit 1 @@ -120,7 +119,7 @@ jobs: echo "${OSSUTIL_SHA256} ${OSSUTIL_ZIP}" | sha256sum -c - unzip -q "${OSSUTIL_ZIP}" sudo install -m 0755 "ossutil-${OSSUTIL_VERSION}-linux-amd64/ossutil" /usr/local/bin/ossutil - - name: Require overwrite-safe bucket versioning state + - name: Require recovery-safe bucket versioning state shell: bash run: | set -euo pipefail @@ -144,11 +143,11 @@ jobs: for child in node: yield from statuses(child) - active = list(statuses(value)) - if active: + active = {status.casefold() for status in statuses(value)} + if active != {"enabled"}: raise SystemExit( - "OSS bucket versioning must be unconfigured because PutObject " - f"forbid-overwrite is ignored when versioning is active or suspended: {active}" + "The shared OSS bucket must have versioning enabled so every Runtime Pack " + f"write receives a recoverable version ID; observed states: {sorted(active)}" ) PY - name: Mirror only immutable version-scoped objects and read them back @@ -157,7 +156,8 @@ jobs: shell: bash run: | set -euo pipefail - mkdir -p readback + mkdir -p readback oss-head + : > oss-version-ids.tsv object_exists() { local object="$1" local listing @@ -177,12 +177,50 @@ jobs: exit 1 } } + record_version_id() { + local name="$1" + local key="runtime-packs/${RELEASE_TAG}/${name}" + local metadata="oss-head/${name}.json" + ossutil api head-object \ + --bucket "${OSS_BUCKET}" \ + --key "${key}" \ + --endpoint "${OSS_ENDPOINT}" \ + --output-format json > "${metadata}" + METADATA_PATH="${metadata}" python - <<'PY' + import json + import os + import re + from pathlib import Path + + value = json.loads(Path(os.environ["METADATA_PATH"]).read_text(encoding="utf-8")) + + def version_ids(node): + if isinstance(node, dict): + for key, child in node.items(): + normalized = re.sub(r"[^a-z0-9]", "", key.casefold()) + if normalized in {"versionid", "xossversionid"} and isinstance(child, str): + if child and child.casefold() != "null": + yield child + yield from version_ids(child) + elif isinstance(node, list): + for child in node: + yield from version_ids(child) + + found = list(dict.fromkeys(version_ids(value))) + if len(found) != 1: + raise SystemExit(f"expected one non-null OSS Version ID, got {found}") + print(found[0]) + PY + } for source in release-dist/*; do name="$(basename "${source}")" object="oss://${OSS_BUCKET}/runtime-packs/${RELEASE_TAG}/${name}" if object_exists "${object}"; then verify_object "${source}" "${name}" - echo "Existing immutable object matches: ${object}" + version_id="$(record_version_id "${name}")" + printf '%s\t%s\t%s\n' "${name}" "runtime-packs/${RELEASE_TAG}/${name}" "${version_id}" \ + >> oss-version-ids.tsv + echo "Existing version-scoped object matches: ${object}" continue else status="$?" @@ -198,11 +236,54 @@ jobs: --endpoint "${OSS_ENDPOINT}"; then if object_exists "${object}"; then verify_object "${source}" "${name}" - continue + else + exit 1 fi - exit 1 fi verify_object "${source}" "${name}" + version_id="$(record_version_id "${name}")" + printf '%s\t%s\t%s\n' "${name}" "runtime-packs/${RELEASE_TAG}/${name}" "${version_id}" \ + >> oss-version-ids.tsv + done + python - <<'PY' + import json + from pathlib import Path + + objects = {} + for line in Path("oss-version-ids.tsv").read_text(encoding="utf-8").splitlines(): + name, key, version_id = line.split("\t") + if name in objects: + raise SystemExit(f"duplicate OSS Version ID entry: {name}") + objects[name] = {"key": key, "versionId": version_id} + expected = {path.name for path in Path("release-dist").iterdir() if path.is_file()} + if objects.keys() != expected: + raise SystemExit( + f"OSS Version ID set mismatch; missing={sorted(expected - objects.keys())}, " + f"extra={sorted(objects.keys() - expected)}" + ) + Path("oss-version-ids.json").write_text( + json.dumps({"objects": objects}, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + PY + - name: Require anonymous client downloads to match the release bytes + env: + RELEASE_TAG: ${{ steps.release.outputs.tag }} + shell: bash + run: | + set -euo pipefail + mkdir public-readback + for source in release-dist/*; do + name="$(basename "${source}")" + url="https://${OSS_BUCKET}.oss-cn-beijing.aliyuncs.com/runtime-packs/${RELEASE_TAG}/${name}" + curl --fail --silent --show-error --location --retry 3 \ + --output "public-readback/${name}" "${url}" + local_sha="$(sha256sum "${source}" | awk '{print $1}')" + public_sha="$(sha256sum "public-readback/${name}" | awk '{print $1}')" + [[ "${local_sha}" == "${public_sha}" ]] || { + echo "Anonymous OSS readback mismatch for ${name}" >&2 + exit 1 + } done - name: Verify GitHub and OSS inventories byte for byte shell: bash @@ -256,3 +337,10 @@ jobs: diff -u \ <(cd release-dist && sha256sum -- * | sort) \ <(cd readback && sha256sum -- * | sort) + - name: Preserve exact OSS version IDs for audit + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: runtime-pack-oss-version-ids-${{ steps.release.outputs.tag }} + path: oss-version-ids.json + if-no-files-found: error + retention-days: 90 diff --git a/README.md b/README.md index 68141d6..7cd753e 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,11 @@ system and CPU architecture. Source archives, versions, and digests are pinned i - Historical Runtime Pack digests must be unique, lowercase SHA-256 values and must never repeat the current pack digest. The first release starts with empty histories. - Git for Windows self-extracting archives are unpacked only inside trusted CI; the - OpenSquilla client never downloads or executes the SFX. + OpenSquilla client never downloads or executes the SFX. The native Windows build + also requires valid Authenticode signatures on the SFX and the probed Git/Bash + executables before any of them can enter a Runtime Pack. +- Every discovered upstream license/notice file is preserved. Explicit per-file, + count, and total-size bounds fail the build instead of silently truncating notices. - Pack extraction rejects traversal, link escapes, special files, duplicate paths, and expansion beyond declared limits. Safe internal upstream links are resolved inside the reviewed archive namespace and materialized as regular files; links never ship @@ -59,19 +63,34 @@ controls: OSS. - Protect the `runtime-pack-release` and `runtime-pack-oss` environments with required reviewers. Only the former may create a Draft Release; it never publishes one. -- Register isolated organization runners with the exact labels declared in - `sources.json`. Every runner needs Python 3.12; Windows runners also need a trusted - 7-Zip CLI. Do not attach these labels to general-purpose or fork-controlled runners. -- Create the `opensquilla-releases` bucket in `cn-beijing` with bucket versioning - unconfigured. OSS ignores `forbid-overwrite` when versioning is enabled or - suspended, so the workflow refuses either state. -- Give the mirror RAM identity only list/read/create access under - `runtime-packs/*`. It must not have `DeleteObject`, unrestricted overwrite, bucket - administration, or access to desktop update-channel paths. Keep the access key only - in the protected `runtime-pack-oss` environment. -- Set `RUNTIME_PACK_OSS_BUCKET=opensquilla-releases` and - `RUNTIME_PACK_OSS_ENDPOINT=https://oss-cn-beijing.aliyuncs.com`. The workflow rejects - other destinations and never writes a moving `latest` or `stable` alias. +- The reviewed matrix uses standard GitHub-hosted native runners declared in + `sources.json`: macOS arm64/x64, Linux arm64/x64, and Windows arm64/x64. This avoids + persistent organization runners and keeps every build on the target architecture. + Windows images must continue to provide the trusted 7-Zip and PowerShell tools used + by the Git Bash extraction and Authenticode gates. +- Reuse the reviewed `opensquilla-releases` bucket in `cn-beijing`, but isolate every + object under `runtime-packs//`. Bucket versioning must remain enabled so + every write has a recoverable Version ID. OSS ignores `forbid-overwrite` on a + versioned bucket, so the workflow never relies on that header: an existing object is + accepted only when its downloaded SHA-256 already matches the GitHub Release byte. +- The workflow itself only lists, reads, and creates objects under `runtime-packs/*`; + it has no delete or desktop update-channel operation. Keep the reused access key + behind required reviewers in the protected `runtime-pack-oss` environment. When RAM + policy changes become available, replace it with a prefix-scoped identity rather + than expanding the existing credential further. +- Allow anonymous `GetObject` only for `runtime-packs/*`; do not grant anonymous + `ListObjects`, write, delete, or bucket-administration permissions. The mirror job + downloads every object again over the exact unsigned public client URL and compares + its SHA-256 before succeeding. +- Expose the existing encrypted `ALIYUN_OSS_ACCESS_KEY_ID` and + `ALIYUN_OSS_ACCESS_KEY_SECRET` to the protected `runtime-pack-oss` environment. + GitHub secrets are repository-scoped, so using the same values in this repository + still requires an environment or organization secret grant; the values are never + copied by a workflow. The bucket and Beijing endpoint are fixed in the workflow, + which never writes a moving `latest` or `stable` alias. +- Preserve the generated `oss-version-ids.json` workflow artifact for each mirror run. + It records the exact current Version ID of every verified object without changing + the immutable GitHub Release. Before approving a Draft Release, reviewers must independently confirm every upstream URL and SHA-256 pin, all fourteen native probe jobs, the exact asset inventory, diff --git a/SECURITY.md b/SECURITY.md index e07169c..01eb04a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,12 +9,25 @@ pin, generated archive, catalog digest, or SBOM is wrong, maintainers must revok draft or affected catalog and publish a new catalog version. Existing bytes must not be replaced under the same tag. -GitHub immutable releases must be enabled before the first publication. The OSS -bucket must remain unversioned so `PutObject` with `forbid-overwrite=true` cannot be -silently converted into a new object version. The mirror identity must not have -delete, bucket-administration, or desktop update-channel permissions. +GitHub immutable releases must be enabled before the first publication. Runtime Packs +reuse the versioned `opensquilla-releases` bucket only under the isolated +`runtime-packs//` prefix. Because OSS ignores `forbid-overwrite` when +versioning is enabled, the mirror treats that header as defense in depth only: it +refuses existing different bytes, verifies authenticated and anonymous downloads, and +records each non-null Version ID. The mirror workflow never writes a desktop release +or update-channel path. If the reused credential currently has broader rights, the +protected environment and required reviewers are compensating controls until a +prefix-scoped RAM identity can replace it; do not grant new delete or bucket-policy +permissions for Runtime Packs. + +The client path requires anonymous `GetObject` only under `runtime-packs/*`. Anonymous +listing and every mutation remain forbidden. Mirroring is successful only after an +unsigned HTTPS download of every object matches the corresponding GitHub Release byte +for byte. The GitHub Draft Release and the protected `runtime-pack-release` and -`runtime-pack-oss` environments require maintainer approval. OSS credentials must be -scoped to `runtime-packs/*` and must not have permission to modify desktop update -channels. +`runtime-pack-oss` environments require maintainer approval. Reusing an existing +credential is an operational compromise, not an integrity dependency: where IAM +changes are available, scope a separate RAM identity to `runtime-packs/*`. Regardless +of credential scope, clients reject any byte sequence whose catalog size or SHA-256 +does not match and try the alternate GitHub/OSS source. diff --git a/scripts/build_pack.py b/scripts/build_pack.py index cb7f45c..4a9d646 100644 --- a/scripts/build_pack.py +++ b/scripts/build_pack.py @@ -30,6 +30,9 @@ MAX_MEMBERS = 250_000 MAX_EXPANSION_RATIO = 400 COPY_CHUNK_BYTES = 1024 * 1024 +MAX_LICENSE_FILES = 10_000 +MAX_LICENSE_FILE_BYTES = 5 * 1024**2 +MAX_LICENSE_TOTAL_BYTES = 128 * 1024**2 LICENSE_NAMES = re.compile(r"^(license|licence|copying|copyright|notice)([._-].*)?$", re.I) MACHINE_ALIASES = { "amd64": "x64", @@ -419,6 +422,50 @@ def _audit_7z_listing_text(listing: str) -> None: raise PackBuildError("7-Zip did not report any Git Bash SFX members") +def _verify_authenticode_signature(path: Path) -> None: + powershell = ( + shutil.which("pwsh") + or shutil.which("powershell") + or shutil.which("powershell.exe") + ) + if powershell is None: + raise PackBuildError("PowerShell is required for the Git Bash Authenticode gate") + environment = os.environ.copy() + environment["OPENSQUILLA_SIGNATURE_PATH"] = str(path.resolve()) + script = ( + "$signature = Get-AuthenticodeSignature -LiteralPath " + "$env:OPENSQUILLA_SIGNATURE_PATH; " + "[PSCustomObject]@{Status=[string]$signature.Status; " + "Subject=[string]$signature.SignerCertificate.Subject} | " + "ConvertTo-Json -Compress" + ) + completed = subprocess.run( + [powershell, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=120, + env=environment, + ) + try: + signature = json.loads(completed.stdout.strip()) + except (json.JSONDecodeError, TypeError) as exc: + raise PackBuildError( + f"cannot audit Authenticode signature for {path.name}: {completed.stdout[-1000:]}" + ) from exc + if ( + completed.returncode != 0 + or not isinstance(signature, Mapping) + or signature.get("Status") != "Valid" + or not isinstance(signature.get("Subject"), str) + or not signature["Subject"].strip() + ): + raise PackBuildError( + f"invalid Authenticode signature for {path.name}: {completed.stdout[-1000:]}" + ) + + def extract_upstream( archive: Path, archive_type: str, @@ -437,6 +484,7 @@ def extract_upstream( ) if platform.system() != "Windows" or strip_components != 0 or not trusted_ci: raise PackBuildError("Git Bash SFX may be unpacked only by native Windows CI") + _verify_authenticode_signature(archive) executable = shutil.which("7z") or shutil.which("7za") if executable is None: raise PackBuildError("7-Zip is required on the trusted Git Bash build runner") @@ -506,6 +554,8 @@ def probe_payload( raise PackBuildError(f"required executable is missing: {relative}") if name not in probe_names: continue + if component_id == "gitBash": + _verify_authenticode_signature(executable) completed = subprocess.run( _probe_command(component_id, name, executable), cwd=payload, @@ -531,19 +581,24 @@ def probe_payload( def collect_licenses(payload: Path, destination: Path) -> None: - candidates = [ - path - for path in payload.rglob("*") - if path.is_file() - and LICENSE_NAMES.fullmatch(path.name) - and path.stat().st_size <= 5 * 1024**2 - ] + candidates: list[Path] = [] + total_bytes = 0 + for path in payload.rglob("*"): + if not path.is_file() or not LICENSE_NAMES.fullmatch(path.name): + continue + size = path.stat().st_size + if size > MAX_LICENSE_FILE_BYTES: + raise PackBuildError(f"upstream license file exceeds the safety limit: {path.name}") + candidates.append(path) + total_bytes += size + if len(candidates) > MAX_LICENSE_FILES or total_bytes > MAX_LICENSE_TOTAL_BYTES: + raise PackBuildError("upstream license set exceeds the safety limit") if not candidates: raise PackBuildError("upstream payload contains no auditable license file") destination.mkdir(parents=True, exist_ok=False) - for index, source in enumerate(sorted(candidates, key=lambda item: item.as_posix())[:100], 1): + for index, source in enumerate(sorted(candidates, key=lambda item: item.as_posix()), 1): relative = source.relative_to(payload).as_posix().replace("/", "__") - target = destination / f"{index:03d}__{relative}" + target = destination / f"{index:05d}__{relative}" shutil.copyfile(source, target) diff --git a/scripts/validate_sources.py b/scripts/validate_sources.py index 1783c83..ace7086 100644 --- a/scripts/validate_sources.py +++ b/scripts/validate_sources.py @@ -20,6 +20,14 @@ "windows-arm64": ("python", "node", "gitBash"), "windows-x64": ("python", "node", "gitBash"), } +TARGET_RUNNERS = { + "darwin-arm64": ["macos-15"], + "darwin-x64": ["macos-15-intel"], + "linux-arm64": ["ubuntu-24.04-arm"], + "linux-x64": ["ubuntu-24.04"], + "windows-arm64": ["windows-11-arm"], + "windows-x64": ["windows-2025"], +} COMPONENT_EXECUTABLES = { "python": {"python"}, "node": {"node", "npm", "npx"}, @@ -186,12 +194,10 @@ def validate_sources(raw: Any) -> dict[str, Any]: if set(target_value) != {"runner", *expected_components}: raise SourceValidationError(f"targets.{target} has an incomplete component matrix") runner = target_value["runner"] - expected_runner = ["self-hosted", "opensquilla-runtime", target] - if target.startswith("linux-"): - expected_runner.append("glibc") + expected_runner = TARGET_RUNNERS[target] if not isinstance(runner, list) or runner != expected_runner: raise SourceValidationError( - f"targets.{target}.runner must exactly identify the native organization runner" + f"targets.{target}.runner must exactly identify the reviewed GitHub-hosted runner" ) for component_id in expected_components: component = target_value[component_id] diff --git a/sources.json b/sources.json index 1c614ea..998c300 100644 --- a/sources.json +++ b/sources.json @@ -1,15 +1,15 @@ { "schemaVersion": 1, - "catalogVersion": "2026-07-30.1", - "releaseTag": "v2026.07.30.1", - "sourceDateEpoch": 1785369600, + "catalogVersion": "2026-08-21.1", + "releaseTag": "v2026.08.21.1", + "sourceDateEpoch": 1787270400, "targets": { "darwin-arm64": { - "runner": ["self-hosted", "opensquilla-runtime", "darwin-arm64"], + "runner": ["macos-15"], "python": { - "version": "3.13.14+20260728", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.13.14%2B20260728-aarch64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "aa2a054f5e04bde63ae199e3bb6bbb634e457423efd294842deeb1299e7e5932", + "version": "3.13.15+20260814", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15%2B20260814-aarch64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "6d472fc49a4d95e58214a992c4c92aa73fe2a935837a01a9a36bab0bec6d72f3", "trustedArchiveSha256": [], "archiveType": "tar.gz", "stripComponents": 1, @@ -17,9 +17,9 @@ "executables": {"python": "bin/python3"} }, "node": { - "version": "24.18.1", - "url": "https://nodejs.org/dist/v24.18.1/node-v24.18.1-darwin-arm64.tar.xz", - "sha256": "1d60b703fe5d7e7072489be8187f430f1a095a658c31e5e1e281331a5873fac3", + "version": "24.19.0", + "url": "https://nodejs.org/dist/v24.19.0/node-v24.19.0-darwin-arm64.tar.xz", + "sha256": "3f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94", "trustedArchiveSha256": [], "archiveType": "tar.xz", "stripComponents": 1, @@ -28,11 +28,11 @@ } }, "darwin-x64": { - "runner": ["self-hosted", "opensquilla-runtime", "darwin-x64"], + "runner": ["macos-15-intel"], "python": { - "version": "3.13.14+20260728", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.13.14%2B20260728-x86_64-apple-darwin-install_only_stripped.tar.gz", - "sha256": "aa73c37aebebe3b7264dce1e49923719ab0ac0fc590353adf393eee3e2041c18", + "version": "3.13.15+20260814", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15%2B20260814-x86_64-apple-darwin-install_only_stripped.tar.gz", + "sha256": "bf87354efcd9ae517da606fcda4e3a3f0d73a6f05ca7cba3c6d3c5270074bfc8", "trustedArchiveSha256": [], "archiveType": "tar.gz", "stripComponents": 1, @@ -40,9 +40,9 @@ "executables": {"python": "bin/python3"} }, "node": { - "version": "24.18.1", - "url": "https://nodejs.org/dist/v24.18.1/node-v24.18.1-darwin-x64.tar.xz", - "sha256": "f892c7895720f40d3750bde24f3554242d36f23602b5167b5b73ec4d13938aef", + "version": "24.19.0", + "url": "https://nodejs.org/dist/v24.19.0/node-v24.19.0-darwin-x64.tar.xz", + "sha256": "d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4", "trustedArchiveSha256": [], "archiveType": "tar.xz", "stripComponents": 1, @@ -51,11 +51,11 @@ } }, "linux-arm64": { - "runner": ["self-hosted", "opensquilla-runtime", "linux-arm64", "glibc"], + "runner": ["ubuntu-24.04-arm"], "python": { - "version": "3.13.14+20260728", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.13.14%2B20260728-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "1eaf979af6c6986553b91a9e3b03647f63ce52a888e00892d3bddc96f43748e9", + "version": "3.13.15+20260814", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15%2B20260814-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "985efd78c1c6521b379f7c64c2a25e6a1130f07441d1af8be441aa05260886aa", "trustedArchiveSha256": [], "archiveType": "tar.gz", "stripComponents": 1, @@ -63,9 +63,9 @@ "executables": {"python": "bin/python3"} }, "node": { - "version": "24.18.1", - "url": "https://nodejs.org/dist/v24.18.1/node-v24.18.1-linux-arm64.tar.xz", - "sha256": "7201e3a09dc825bac57867c81913e2b8f0ef87d04cb9082af4cda82f6ff3d88c", + "version": "24.19.0", + "url": "https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-arm64.tar.xz", + "sha256": "01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc", "trustedArchiveSha256": [], "archiveType": "tar.xz", "stripComponents": 1, @@ -74,11 +74,11 @@ } }, "linux-x64": { - "runner": ["self-hosted", "opensquilla-runtime", "linux-x64", "glibc"], + "runner": ["ubuntu-24.04"], "python": { - "version": "3.13.14+20260728", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.13.14%2B20260728-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", - "sha256": "6734c3e643c75e860c36ee3a7904e8e6bafbf3232d89b17ffd5fbfa72ab2816c", + "version": "3.13.15+20260814", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15%2B20260814-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz", + "sha256": "aaca2af2ab4d7b68a712660d1334c0cfd5ec13c0312ccd30c29122d8d0342320", "trustedArchiveSha256": [], "archiveType": "tar.gz", "stripComponents": 1, @@ -86,9 +86,9 @@ "executables": {"python": "bin/python3"} }, "node": { - "version": "24.18.1", - "url": "https://nodejs.org/dist/v24.18.1/node-v24.18.1-linux-x64.tar.xz", - "sha256": "d6c664df3f3f61458e8c277585571328522d705166723a7c7823a9253a4d15a0", + "version": "24.19.0", + "url": "https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-x64.tar.xz", + "sha256": "14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647", "trustedArchiveSha256": [], "archiveType": "tar.xz", "stripComponents": 1, @@ -97,11 +97,11 @@ } }, "windows-arm64": { - "runner": ["self-hosted", "opensquilla-runtime", "windows-arm64"], + "runner": ["windows-11-arm"], "python": { - "version": "3.13.14+20260728", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.13.14%2B20260728-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "e28e7108a4b36c0c321da8c842a7addf59358e26b5ec9abe002e9e940130f41f", + "version": "3.13.15+20260814", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15%2B20260814-aarch64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "f6bf0fa39ad3668dc1996052ec6eff58c54aa126bf203cf3728a47a830af2b5d", "trustedArchiveSha256": [], "archiveType": "tar.gz", "stripComponents": 1, @@ -109,9 +109,9 @@ "executables": {"python": "python.exe"} }, "node": { - "version": "24.18.1", - "url": "https://nodejs.org/dist/v24.18.1/node-v24.18.1-win-arm64.zip", - "sha256": "ffbc7d3e1baf6804f7431ff94f19b9a885a650568c93ea4ccb1bb0038f6af825", + "version": "24.19.0", + "url": "https://nodejs.org/dist/v24.19.0/node-v24.19.0-win-arm64.zip", + "sha256": "8502f4a50b458d4cc38ed8f2001556c2cd239d464920f74017926ccb1e1c157f", "trustedArchiveSha256": [], "archiveType": "zip", "stripComponents": 1, @@ -119,9 +119,9 @@ "executables": {"node": "node.exe", "npm": "npm.cmd", "npx": "npx.cmd"} }, "gitBash": { - "version": "2.55.0.windows.3", - "url": "https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.3/PortableGit-2.55.0.3-arm64.7z.exe", - "sha256": "3bf26b94d9399b16a890776e468334f501742861576cbcdea2d9134643c374bd", + "version": "2.55.0.windows.5", + "url": "https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.5/PortableGit-2.55.0.5-arm64.7z.exe", + "sha256": "49d1dd3158017fa9805d07268433dbab7021b2ec1c1cc3fbabaf8b8255764dd0", "trustedArchiveSha256": [], "archiveType": "7z-sfx", "stripComponents": 0, @@ -130,11 +130,11 @@ } }, "windows-x64": { - "runner": ["self-hosted", "opensquilla-runtime", "windows-x64"], + "runner": ["windows-2025"], "python": { - "version": "3.13.14+20260728", - "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.13.14%2B20260728-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", - "sha256": "a091ab914f2b7d2dbc52e9cf4a225190f72709fc79a64ec44bc61ca4d2908a64", + "version": "3.13.15+20260814", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15%2B20260814-x86_64-pc-windows-msvc-install_only_stripped.tar.gz", + "sha256": "07c977bbe4abad07e3bbc314608633e6c74eab482a7bae81f4361cda970b45e6", "trustedArchiveSha256": [], "archiveType": "tar.gz", "stripComponents": 1, @@ -142,9 +142,9 @@ "executables": {"python": "python.exe"} }, "node": { - "version": "24.18.1", - "url": "https://nodejs.org/dist/v24.18.1/node-v24.18.1-win-x64.zip", - "sha256": "ec56b84a7551893ab2324ebdfdc4ab974a63b4781162600b68a1293cc3e53765", + "version": "24.19.0", + "url": "https://nodejs.org/dist/v24.19.0/node-v24.19.0-win-x64.zip", + "sha256": "57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73", "trustedArchiveSha256": [], "archiveType": "zip", "stripComponents": 1, @@ -152,9 +152,9 @@ "executables": {"node": "node.exe", "npm": "npm.cmd", "npx": "npx.cmd"} }, "gitBash": { - "version": "2.55.0.windows.3", - "url": "https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.3/PortableGit-2.55.0.3-64-bit.7z.exe", - "sha256": "ab00566336b5472120f9a52d34f2e79c5406535792acb0548001ffd0bd090e5d", + "version": "2.55.0.windows.5", + "url": "https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.5/PortableGit-2.55.0.5-64-bit.7z.exe", + "sha256": "5aa8a20f6e9abb2c755f0e73c91c687701a46b309ad84a0ca6509380fa4ae290", "trustedArchiveSha256": [], "archiveType": "7z-sfx", "stripComponents": 0, diff --git a/tests/test_pack_safety.py b/tests/test_pack_safety.py index 883aee5..5e1fc89 100644 --- a/tests/test_pack_safety.py +++ b/tests/test_pack_safety.py @@ -3,6 +3,7 @@ import hashlib import io import json +import subprocess import tarfile from pathlib import Path @@ -13,6 +14,8 @@ _audit_7z_listing_text, _deterministic_tar_xz, _extract_tar, + _verify_authenticode_signature, + collect_licenses, sha256_file, ) from scripts.generate_release import ( @@ -96,6 +99,66 @@ def test_git_sfx_listing_rejects_traversal_and_links() -> None: _audit_7z_listing_text("Path = bin/bash.exe\nSymbolic Link = ../../outside\n") +def test_authenticode_gate_accepts_only_a_valid_named_signer( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + signed = tmp_path / "PortableGit.exe" + signed.write_bytes(b"signed") + monkeypatch.setattr("scripts.build_pack.shutil.which", lambda _name: "pwsh") + + def run(*_args: object, **kwargs: object) -> subprocess.CompletedProcess[str]: + assert kwargs["env"]["OPENSQUILLA_SIGNATURE_PATH"] == str(signed.resolve()) + return subprocess.CompletedProcess( + args=["pwsh"], + returncode=0, + stdout=json.dumps({"Status": "Valid", "Subject": "CN=Git for Windows"}), + ) + + monkeypatch.setattr("scripts.build_pack.subprocess.run", run) + _verify_authenticode_signature(signed) + + +@pytest.mark.parametrize( + ("returncode", "signature"), + [ + (0, {"Status": "NotSigned", "Subject": ""}), + (0, {"Status": "Valid", "Subject": ""}), + (1, {"Status": "Valid", "Subject": "CN=Git for Windows"}), + ], +) +def test_authenticode_gate_rejects_invalid_results( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + returncode: int, + signature: dict[str, str], +) -> None: + signed = tmp_path / "PortableGit.exe" + signed.write_bytes(b"unsigned") + monkeypatch.setattr("scripts.build_pack.shutil.which", lambda _name: "pwsh") + monkeypatch.setattr( + "scripts.build_pack.subprocess.run", + lambda *_args, **_kwargs: subprocess.CompletedProcess( + args=["pwsh"], returncode=returncode, stdout=json.dumps(signature) + ), + ) + with pytest.raises(PackBuildError, match="invalid Authenticode signature"): + _verify_authenticode_signature(signed) + + +def test_collect_licenses_preserves_more_than_one_hundred_files(tmp_path: Path) -> None: + payload = tmp_path / "payload" + for index in range(101): + path = payload / f"dependency-{index:03d}" / "LICENSE" + path.parent.mkdir(parents=True) + path.write_text(f"license {index}\n", encoding="utf-8") + destination = tmp_path / "licenses" + collect_licenses(payload, destination) + copied = sorted(destination.iterdir()) + assert len(copied) == 101 + assert copied[0].name.startswith("00001__") + assert copied[-1].name.startswith("00101__") + + def test_deterministic_tar_has_identical_bytes(tmp_path: Path) -> None: source = tmp_path / "source" (source / "payload" / "bin").mkdir(parents=True) diff --git a/tests/test_validate_sources.py b/tests/test_validate_sources.py index 15cd83b..4ab6ccf 100644 --- a/tests/test_validate_sources.py +++ b/tests/test_validate_sources.py @@ -17,7 +17,7 @@ def _valid() -> dict: def test_checked_in_sources_are_complete_and_native() -> None: value = load_sources(ROOT / "sources.json") - assert value["catalogVersion"] == "2026-07-30.1" + assert value["catalogVersion"] == "2026-08-21.1" assert set(value["targets"]) == { "darwin-arm64", "darwin-x64", @@ -45,8 +45,8 @@ def test_rejects_non_https_or_unapproved_origin() -> None: def test_rejects_missing_native_runner_label() -> None: value = _valid() - value["targets"]["windows-arm64"]["runner"] = ["self-hosted", "opensquilla-runtime"] - with pytest.raises(SourceValidationError, match="native organization runner"): + value["targets"]["windows-arm64"]["runner"] = ["windows-latest"] + with pytest.raises(SourceValidationError, match="reviewed GitHub-hosted runner"): validate_sources(value) diff --git a/tests/test_workflow_contract.py b/tests/test_workflow_contract.py index 8621fdf..29a6589 100644 --- a/tests/test_workflow_contract.py +++ b/tests/test_workflow_contract.py @@ -51,11 +51,11 @@ def test_release_workflow_can_only_create_a_draft() -> None: assert "OPENSQUILLA_TRUSTED_WINDOWS_CI" in workflow -def test_oss_workflow_has_no_moving_alias_and_uses_separate_credentials() -> None: +def test_oss_workflow_has_no_moving_alias_and_uses_reviewed_shared_bucket() -> None: workflow, _parsed = _workflow("mirror-oss.yml") assert "runtime-packs/${RELEASE_TAG}/${name}" in workflow - assert "RUNTIME_PACK_OSS_ACCESS_KEY_ID" in workflow - assert "RUNTIME_PACK_OSS_ACCESS_KEY_SECRET" in workflow + assert "ALIYUN_OSS_ACCESS_KEY_ID" in workflow + assert "ALIYUN_OSS_ACCESS_KEY_SECRET" in workflow assert "stable.json" not in workflow assert "latest.json" not in workflow assert "/latest/" not in workflow @@ -63,8 +63,16 @@ def test_oss_workflow_has_no_moving_alias_and_uses_separate_credentials() -> Non assert "get-bucket-versioning" in workflow assert "list-objects-v2" in workflow assert "OSS exact object set mismatch" in workflow + assert "Require anonymous client downloads to match the release bytes" in workflow + assert "https://${OSS_BUCKET}.oss-cn-beijing.aliyuncs.com/runtime-packs/" in workflow + assert "Anonymous OSS readback mismatch" in workflow assert '"opensquilla-releases"' in workflow + assert '"opensquilla-runtime-packs"' not in workflow assert '"https://oss-cn-beijing.aliyuncs.com"' in workflow + assert "head-object" in workflow + assert "expected one non-null OSS Version ID" in workflow + assert "oss-version-ids.json" in workflow + assert 'active != {"enabled"}' in workflow def test_all_external_actions_are_pinned_to_full_commit_sha() -> None: @@ -83,8 +91,8 @@ def test_every_workflow_is_valid_yaml() -> None: def test_catalog_identity_matches_release_tag_contract() -> None: sources = json.loads((ROOT / "sources.json").read_text(encoding="utf-8")) - assert sources["catalogVersion"] == "2026-07-30.1" - assert sources["releaseTag"] == "v2026.07.30.1" + assert sources["catalogVersion"] == "2026-08-21.1" + assert sources["releaseTag"] == "v2026.08.21.1" def test_repository_publishes_the_complete_apache_2_license() -> None: