diff --git a/.config/nextest.toml b/.config/nextest.toml index 55745be..c7fe19b 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,3 +1,16 @@ +nextest-version = "0.9.140" + +# Core tests are platform-independent library decisions and contract binaries. +# Tests under tests/ default to the host profile unless deliberately named contract_*. +[profile.ci-core] +fail-fast = false +default-filter = 'binary(/^contract_/) | (binary(gitveil) & not test(/^(configure::tests|git::repository::tests|identity::tests|runtime::sidecar::tests)/))' + +# Host tests are the exact complement of core tests and exercise external effects. +[profile.ci-host] +fail-fast = false +default-filter = 'not (binary(/^contract_/) | (binary(gitveil) & not test(/^(configure::tests|git::repository::tests|identity::tests|runtime::sidecar::tests)/)))' + # Nextest runs every test in its own process, so serial_test's in-process # lock does not serialize across tests here. This group preserves the same # serialization the `#[serial]` markers provide under `cargo test`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cf1d9e..22943b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,86 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +defaults: + run: + shell: bash + +env: + CARGO_INCREMENTAL: "0" + CARGO_TERM_COLOR: always + PYTHONDONTWRITEBYTECODE: "1" + jobs: - quality: + core: + name: Core + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Check out the repository + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Activate the pinned Rust toolchain + run: | + rustc --version + cargo --version + + - name: Install pinned quality tools + uses: taiki-e/install-action@6cd13508893c0e7eab5f273c2575d3859bd7229a # v2 + with: + tool: | + cargo-nextest@0.9.140 + cargo-deny@0.20.2 + checksum: true + fallback: none + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Check functional-core boundaries + run: ./scripts/check-functional-core.py + + - name: Check documentation links + run: ./scripts/check-doc-links.py + + - name: Run Clippy + run: cargo clippy --all-targets --all-features --locked -- -D warnings + + - name: Verify the test partition + run: | + partition="$(mktemp -d)" + trap 'rm -rf "$partition"' EXIT + cargo nextest list --profile ci-core --all-features --locked | sort -u > "$partition/core" + cargo nextest list --profile ci-host --all-features --locked | sort -u > "$partition/host" + cargo nextest list --all-features --locked | sort -u > "$partition/all" + comm -12 "$partition/core" "$partition/host" > "$partition/overlap" + if [[ -s "$partition/overlap" ]]; then + echo "Core and host test profiles overlap" >&2 + exit 1 + fi + cat "$partition/core" "$partition/host" | sort -u > "$partition/combined" + diff -u "$partition/all" "$partition/combined" + + - name: Run core tests + run: cargo nextest run --profile ci-core --all-features --locked --no-tests fail + + - name: Check dependency policy + run: cargo deny check + + - name: Check patch whitespace + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.sha }} + run: | + if [[ -n "$BASE_SHA" && "$BASE_SHA" != "0000000000000000000000000000000000000000" ]]; then + git diff --check "$BASE_SHA" "$HEAD_SHA" + else + git diff-tree --check --root --no-commit-id -r "$HEAD_SHA" + fi + + host: name: ${{ matrix.name }} strategy: fail-fast: false @@ -27,27 +105,47 @@ jobs: os: macos-latest runs-on: ${{ matrix.os }} timeout-minutes: 90 - env: - CARGO_INCREMENTAL: "0" - CARGO_TERM_COLOR: always - PYTHONDONTWRITEBYTECODE: "1" steps: - name: Check out the repository uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false - name: Activate the pinned Rust toolchain run: | rustc --version cargo --version - - name: Install pinned quality tools + - name: Install pinned test runner uses: taiki-e/install-action@6cd13508893c0e7eab5f273c2575d3859bd7229a # v2 with: - tool: | - cargo-nextest@0.9.140 - cargo-deny@0.20.2 + tool: cargo-nextest@0.9.140 checksum: true fallback: none - - name: Run the native host quality gate - run: ./scripts/check-host.sh + - name: Fetch checksum-pinned test sidecars + run: | + sops_bin="$(./scripts/fetch-test-sops.py)" + age_keygen_bin="$(./scripts/fetch-test-age-keygen.py)" + age_keygen_archive="$(./scripts/fetch-test-age-keygen.py --print-archive)" + { + echo "SOPS_BIN=$sops_bin" + echo "AGE_KEYGEN_BIN=$age_keygen_bin" + echo "AGE_KEYGEN_ARCHIVE=$age_keygen_archive" + } >> "$GITHUB_ENV" + + - name: Run native host tests + run: cargo nextest run --profile ci-host --all-features --locked --no-tests fail + + - name: Build the native release binary + run: cargo build --release --locked + + - name: Validate the native release package + run: | + ./scripts/build-release-archive.py \ + --skip-build \ + --binary target/release/gitveil \ + --sops-bin "$SOPS_BIN" \ + --age-keygen-bin "$AGE_KEYGEN_BIN" \ + --age-keygen-archive "$AGE_KEYGEN_ARCHIVE" \ + --output-dir target/package-test diff --git a/AGENTS.md b/AGENTS.md index 04ceca7..28cf80b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,8 @@ Inspect the existing implementation and tests before proposing changes. Do not i - Gitveil is one Rust 2024 package with a library and a thin binary. - Supported production hosts are macOS and Linux. - The Rust version is pinned in `rust-toolchain.toml`. -- Compatible SOPS and age-keygen builds and artifact checksums are declared in `scripts/sops_artifacts.py` and `scripts/age_artifacts.py`. +- Compatible SOPS and age-keygen builds and artifact checksums are declared in `scripts/_lib/sops_artifacts.py` and `scripts/_lib/age_keygen_artifacts.py`. +- Files directly under `scripts/` are executable entry points; import-only Python modules belong under `scripts/_lib/` and must not be executable. - `Cargo.toml` intentionally has `publish = false`; do not publish the crate or create releases unless explicitly requested. - Keep all tracked text, code comments, tests, commit messages, and pull request content in English. - Do not add private keys, plaintext secrets, local absolute paths, generated build output, unpublished planning material, or tool transcripts. @@ -29,7 +30,7 @@ Inspect the existing implementation and tests before proposing changes. Do not i Keep pure policy and transformation code separate from external effects. - Pure domain modules and feature-local `plan` modules must not access the filesystem, processes, Git, SOPS, environment, network, clock, threads, or randomness. -- `scripts/check-architecture.py` is the executable declaration of modules that belong to the functional core. Add new pure modules to that declaration rather than bypassing the check. +- `scripts/check-functional-core.py` is the executable declaration of modules that belong to the functional core. Add new pure modules to that declaration rather than bypassing the check. - `src/source/` owns typed and lossless source parsing and rendering. - `src/semantic/` owns pure semantic diff and merge behavior. - `src/git/`, `src/sops/`, `src/age.rs`, and `src/runtime/` contain external adapters. @@ -80,15 +81,13 @@ Match the test level to the contract: - Isolate `HOME`, global and system Git configuration, identity environment variables, and fixture state. - Every child process needs a deadline and must be killed and reaped after a timeout. -Use targeted tests during development. The public gates are: +Use targeted tests during development. Complete validation runs only in GitHub Actions: -```bash -./scripts/check-host.sh -./scripts/check-linux.sh -./scripts/check-all.sh -``` +- `Core` runs platform-independent library unit tests, pure `contract_*` binaries, formatting, architecture and documentation checks, Clippy, and dependency policy once on Ubuntu. +- `Linux` and `macOS` each run the complementary native host suite, release build, and package validation on a GitHub-hosted runner. +- `.config/nextest.toml` is the executable test classification. New `tests/*.rs` binaries default to the host suite unless deliberately named `contract_*`; colocated unit tests must remain pure unless their adapter module is explicitly assigned to the host profile. -`check-host.sh` is the native macOS/Linux gate. `check-linux.sh` runs the Linux suite in Docker. `check-all.sh` runs both and therefore must be invoked from macOS. GitHub Actions runs `check-host.sh` natively on Linux and macOS for pull requests and pushes to `main`. +Do not recreate aggregate local quality-gate scripts or Docker replicas of GitHub-hosted runners. Local commands are for targeted Red/Green feedback; the pull request checks are the complete merge gates. Do not change test expectations merely to make a gate pass. Fix the implementation, fixture isolation, or contract. @@ -97,7 +96,7 @@ Do not change test expectations merely to make a gate pass. Fix the implementati - Update `README.md` when commands, configuration, installation, security boundaries, or release layout change. - Update `docs/getting-started.md` when the first-use path changes. - Update `CONTRIBUTING.md` when contributor tooling or quality gates change. -- Keep relative links in public Markdown valid; `scripts/check-docs.py` enforces them. +- Keep relative links in public Markdown valid; `scripts/check-doc-links.py` enforces them. - Keep dependency changes intentional and locked. Explain why a new runtime dependency belongs at its architectural boundary. - Preserve third-party license files and notices in release archives. @@ -108,7 +107,7 @@ Use English branch names, commits, pull request titles, and descriptions. Keep e - a pull request, - linear history through squash merging, - resolved review conversations, -- successful `Linux` and `macOS` checks, +- successful `Core`, `Linux`, and `macOS` checks, - a branch tested against the latest `main`. Before handing off a change, inspect the final diff, run `git diff --check`, confirm that no generated or secret-bearing files are tracked, and report the exact checks run and any checks that could not be run. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f290ba..0e08106 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,24 +4,14 @@ Gitveil handles secret-bearing files and Git history, so changes must preserve i ## Development environment -Required tools: +Required tools for source development: - macOS or Linux - Git 2.20+ - Rust 1.98.0, selected automatically by `rust-toolchain.toml` - Python 3.9+ -- `cargo-nextest` 0.9.140 -- `cargo-deny` -- Docker for the Linux quality gate -Install the Cargo tools if they are not already available: - -```bash -cargo install cargo-nextest --locked --version 0.9.140 -cargo install cargo-deny --locked -``` - -The test and packaging scripts download checksum-pinned SOPS and age artifacts into `target/` on first use. They do not modify system installations. +GitHub Actions installs the pinned `cargo-nextest` and `cargo-deny` versions used by the complete merge gates. Artifact helpers download checksum-pinned SOPS and age builds for the current platform into `target/`; they do not modify system installations. ## Making changes @@ -44,27 +34,24 @@ Preserve these invariants: ## Tests -Run the host gate for the current platform: +Use targeted commands for local Red/Green feedback. Fetch only the current platform's pinned sidecars when the selected test needs them: ```bash -./scripts/check-host.sh +./scripts/fetch-test-sops.py +AGE_KEYGEN_BIN="$(./scripts/fetch-test-age-keygen.py)" \ + cargo test --test integration_identity +cargo test open::plan::tests::both_sides_changed_reports_a_conflict_and_keeps_local ``` -Run the native Linux suite in Docker: - -```bash -./scripts/check-linux.sh -``` - -On macOS, run both through the unified gate before requesting review: - -```bash -./scripts/check-all.sh -``` +Complete validation runs only in GitHub Actions. `.config/nextest.toml` defines two complementary suites: -The gates cover formatting, architecture boundaries, public documentation links, Clippy, unit and integration tests, dependency policy, release builds, and native archive validation. Integration tests use real Git and checksum-pinned SOPS and age-keygen executables. Initial cold runs download dependencies and build artifacts and may take substantially longer than warm runs. +| Check | Runner | Scope | +|---|---|---| +| `Core` | Ubuntu | Platform-independent unit and `contract_*` tests, formatting, architecture, documentation, Clippy, and dependency policy | +| `Linux` | Ubuntu | Native CLI, Git, SOPS, identity, filesystem, runtime, release build, and package validation | +| `macOS` | macOS | The same native host suite on macOS | -GitHub Actions runs `scripts/check-host.sh` natively on Linux and macOS for every pull request and push to `main`. CI uses no repository secrets and does not publish artifacts or releases. +Every pull request and push to `main` runs all three checks on GitHub-hosted runners. CI uses no repository secrets and does not publish artifacts or releases. Do not add aggregate local gate scripts or a Docker replica of the hosted Linux runner. ## Pull requests @@ -74,6 +61,6 @@ A pull request should: - Include tests for each changed postcondition and relevant failure path. - Update user documentation when installation, commands, output, configuration, security boundaries, or release layout changes. - Contain no private identity, plaintext secret, local absolute path, generated build output, or unrelated formatting churn. -- Pass the applicable host and Linux quality gates. +- Pass the required `Core`, `Linux`, and `macOS` GitHub Actions checks. By intentionally submitting a contribution for inclusion in Gitveil, you agree to license that contribution under the MIT License without additional terms or conditions. diff --git a/README.md b/README.md index e32b47c..99f96e0 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,13 @@ Gitveil does not require a system SOPS or age executable. Release archives inclu Install from a source checkout for the current user: ```bash -./scripts/install-local.py +./scripts/install-from-source.py ``` The default prefix is `${CARGO_HOME:-$HOME/.cargo}`. To select another prefix: ```bash -./scripts/install-local.py --prefix ~/.local +./scripts/install-from-source.py --prefix ~/.local ``` The installer performs a locked release build, fetches and verifies official SOPS 3.13.3 and age-keygen 1.3.1 artifacts, validates the complete release archive, and transactionally installs the binary, private sidecars, and license files. Do not use `cargo install --path .`: Cargo installs only the binary and omits the private sidecars. @@ -31,7 +31,7 @@ The installer performs a locked release build, fetches and verifies official SOP The user-facing installation unit is a native release archive. Maintainers build one on each target platform with: ```bash -python3 scripts/package-release.py +./scripts/build-release-archive.py ``` The archive is written to `dist/gitveil-v--.tar.gz` with this layout: @@ -187,13 +187,7 @@ Gitveil reads index stages but never runs `git add`. ## Development -```bash -./scripts/check-host.sh -./scripts/check-linux.sh -./scripts/check-all.sh -``` - -The unified local quality gate covers the macOS host, Docker Linux, real SOPS compatibility, release archive structure, and native release builds. GitHub Actions runs the native host gate on Linux and macOS for every pull request and push to `main`. +Use targeted `cargo test` commands for local development. Complete validation runs in GitHub Actions for every pull request and push to `main`: `Core` runs platform-independent tests and static quality checks once, while `Linux` and `macOS` run the complementary native host tests, release builds, and package validation. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the test classification and contributor workflow. ## License diff --git a/docker/test.Dockerfile b/docker/test.Dockerfile deleted file mode 100644 index 8e10e1a..0000000 --- a/docker/test.Dockerfile +++ /dev/null @@ -1,93 +0,0 @@ -FROM rust:1.98-bookworm@sha256:e70e2eec3d495fd5c8e0be74adda86507dfac7f51a724fbf9813ff59b2b247c7 - -# RUST_TOOLCHAIN is derived from rust-toolchain.toml by scripts/check-linux.sh so -# the pinned toolchain has a single source of truth. The base image tag only has -# to satisfy the same major.minor; rustup fetches the exact patch release. -ARG RUST_TOOLCHAIN -ARG SOPS_VERSION=3.13.3 -ARG SOPS_ARTIFACT -ARG SOPS_SHA256 -# The compatibility baseline is a test-only dependency: the suite replays -# ciphertext written by this build through the pinned sidecar. -ARG SOPS_BASELINE_VERSION=3.13.2 -ARG SOPS_BASELINE_ARTIFACT -ARG SOPS_BASELINE_SHA256 -# age-keygen is the pinned identity sidecar used by integration and packaging tests. -ARG AGE_VERSION=1.3.1 -ARG AGE_ARTIFACT -ARG AGE_SHA256 -ARG NEXTEST_VERSION=0.9.140 -ENV SOPS_BIN=/usr/local/bin/sops \ - SOPS_BASELINE_BIN=/usr/local/bin/sops-baseline \ - SOPS_DISABLE_VERSION_CHECK=1 \ - AGE_KEYGEN_BIN=/usr/local/bin/age-keygen \ - AGE_KEYGEN_ARCHIVE=/usr/local/share/gitveil-test/age.tar.gz - -RUN apt-get update \ - && apt-get install --yes --no-install-recommends ca-certificates curl git python3 \ - && rm -rf /var/lib/apt/lists/* - -RUN set -eu; \ - test -n "${RUST_TOOLCHAIN}"; \ - rustup toolchain install "${RUST_TOOLCHAIN}" --profile minimal --component clippy --component rustfmt; \ - rustup default "${RUST_TOOLCHAIN}"; \ - rustc --version - -RUN set -eu; \ - test -n "${SOPS_ARTIFACT}"; \ - test -n "${SOPS_SHA256}"; \ - curl --fail --location --silent --show-error \ - "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/${SOPS_ARTIFACT}" \ - --output /usr/local/bin/sops; \ - echo "${SOPS_SHA256} /usr/local/bin/sops" | sha256sum --check --strict; \ - chmod 0755 /usr/local/bin/sops; \ - sops --disable-version-check --version - -RUN set -eu; \ - test -n "${SOPS_BASELINE_ARTIFACT}"; \ - test -n "${SOPS_BASELINE_SHA256}"; \ - curl --fail --location --silent --show-error \ - "https://github.com/getsops/sops/releases/download/v${SOPS_BASELINE_VERSION}/${SOPS_BASELINE_ARTIFACT}" \ - --output /usr/local/bin/sops-baseline; \ - echo "${SOPS_BASELINE_SHA256} /usr/local/bin/sops-baseline" | sha256sum --check --strict; \ - chmod 0755 /usr/local/bin/sops-baseline; \ - /usr/local/bin/sops-baseline --disable-version-check --version - -RUN set -eu; \ - test -n "${AGE_ARTIFACT}"; \ - test -n "${AGE_SHA256}"; \ - curl --fail --location --silent --show-error \ - "https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/${AGE_ARTIFACT}" \ - --output /tmp/age.tar.gz; \ - echo "${AGE_SHA256} /tmp/age.tar.gz" | sha256sum --check --strict; \ - tar -xzf /tmp/age.tar.gz -C /tmp age/age-keygen; \ - install -m 0755 /tmp/age/age-keygen /usr/local/bin/age-keygen; \ - mkdir -p /usr/local/share/gitveil-test; \ - install -m 0644 /tmp/age.tar.gz /usr/local/share/gitveil-test/age.tar.gz; \ - rm -rf /tmp/age /tmp/age.tar.gz; \ - age-keygen --version - -RUN set -eu; \ - case "$(uname -m)" in \ - x86_64) \ - artifact="linux"; \ - checksum="4ee9aaa0d0171a985a5d0eb735b87355894c1c455972e9674fb9fdbd1387c9a3" \ - ;; \ - aarch64|arm64) \ - artifact="linux-arm"; \ - checksum="8b3f4d4560b6b0f83774fecc6be07e47716dbad0eb0bb6c3890f478f4affe4b6" \ - ;; \ - *) \ - echo "unsupported Docker architecture: $(uname -m)" >&2; \ - exit 1 \ - ;; \ - esac; \ - curl --fail --location --silent --show-error \ - "https://get.nexte.st/${NEXTEST_VERSION}/${artifact}" \ - --output /tmp/nextest.tar.gz; \ - echo "${checksum} /tmp/nextest.tar.gz" | sha256sum --check --strict; \ - tar -xzf /tmp/nextest.tar.gz -C /usr/local/cargo/bin; \ - rm /tmp/nextest.tar.gz; \ - cargo nextest --version - -WORKDIR /work diff --git a/scripts/_lib/__init__.py b/scripts/_lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/age_artifacts.py b/scripts/_lib/age_keygen_artifacts.py old mode 100755 new mode 100644 similarity index 93% rename from scripts/age_artifacts.py rename to scripts/_lib/age_keygen_artifacts.py index 7eb69f1..8ffb485 --- a/scripts/age_artifacts.py +++ b/scripts/_lib/age_keygen_artifacts.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 """Pinned official age-keygen artifacts shared by tests and release packaging.""" from __future__ import annotations -import argparse from dataclasses import dataclass import hashlib from pathlib import Path @@ -191,16 +189,3 @@ def fetch_verified(destination: Path, artifact: Artifact) -> Path: extracted.unlink(missing_ok=True) raise return extracted - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--print-version", action="store_true") - parser.add_argument("--system") - parser.add_argument("--machine") - arguments = parser.parse_args() - if arguments.print_version: - print(VERSION) - else: - selected = artifact_for(arguments.system, arguments.machine) - print(f"{selected.filename}\t{selected.sha256}") diff --git a/scripts/release_package.py b/scripts/_lib/release_archive.py similarity index 95% rename from scripts/release_package.py rename to scripts/_lib/release_archive.py index 6a66561..735b0f7 100644 --- a/scripts/release_package.py +++ b/scripts/_lib/release_archive.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """Shared native release archive construction for Gitveil packaging and installation.""" from __future__ import annotations @@ -12,12 +11,12 @@ import tarfile import tempfile -from age_artifacts import VERSION as AGE_VERSION -from age_artifacts import artifact_for as age_artifact_for -from age_artifacts import fetch_verified as fetch_age_keygen -from age_artifacts import verify_binary as verify_age_keygen -from sops_artifacts import VERSION as SOPS_VERSION -from sops_artifacts import artifact_for, fetch_verified, verify +from .age_keygen_artifacts import VERSION as AGE_VERSION +from .age_keygen_artifacts import artifact_for as age_artifact_for +from .age_keygen_artifacts import fetch_verified as fetch_age_keygen +from .age_keygen_artifacts import verify_binary as verify_age_keygen +from .sops_artifacts import VERSION as SOPS_VERSION +from .sops_artifacts import artifact_for, fetch_verified, verify def package_version(root: Path) -> str: diff --git a/scripts/sops_artifacts.py b/scripts/_lib/sops_artifacts.py similarity index 84% rename from scripts/sops_artifacts.py rename to scripts/_lib/sops_artifacts.py index 53dadaf..12522d9 100644 --- a/scripts/sops_artifacts.py +++ b/scripts/_lib/sops_artifacts.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 -"""Pinned official SOPS artifacts shared by tests, Docker, and release packaging.""" +"""Pinned official SOPS artifacts shared by tests and release packaging.""" from __future__ import annotations -import argparse from dataclasses import dataclass import hashlib from pathlib import Path @@ -165,26 +163,3 @@ def fetch_verified(destination: Path, artifact: Artifact) -> Path: temporary.unlink(missing_ok=True) raise return destination - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--print-version", - action="store_true", - help="print the selected SOPS version instead of its artifact metadata", - ) - parser.add_argument( - "--baseline", - action="store_true", - help="select the compatibility baseline instead of the pinned sidecar", - ) - parser.add_argument("--system") - parser.add_argument("--machine") - arguments = parser.parse_args() - selected_version = COMPATIBILITY_BASELINE_VERSION if arguments.baseline else VERSION - if arguments.print_version: - print(selected_version) - else: - selected = artifact_for(arguments.system, arguments.machine, selected_version) - print(f"{selected.filename}\t{selected.sha256}") diff --git a/scripts/package-release.py b/scripts/build-release-archive.py old mode 100644 new mode 100755 similarity index 95% rename from scripts/package-release.py rename to scripts/build-release-archive.py index 93d2062..128d13f --- a/scripts/package-release.py +++ b/scripts/build-release-archive.py @@ -7,8 +7,8 @@ from pathlib import Path import subprocess -from release_package import build_archive -from sops_artifacts import digest +from _lib.release_archive import build_archive +from _lib.sops_artifacts import digest def main() -> int: diff --git a/scripts/check-all.sh b/scripts/check-all.sh deleted file mode 100755 index 9d6bb14..0000000 --- a/scripts/check-all.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -set -eu - -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) - -if [ "$(uname -s)" != "Darwin" ]; then - echo "The combined macOS/Linux quality gate must run on a macOS host" >&2 - exit 2 -fi - -"$ROOT/scripts/check-host.sh" -"$ROOT/scripts/check-linux.sh" diff --git a/scripts/check-docs.py b/scripts/check-doc-links.py similarity index 100% rename from scripts/check-docs.py rename to scripts/check-doc-links.py diff --git a/scripts/check-architecture.py b/scripts/check-functional-core.py old mode 100644 new mode 100755 similarity index 100% rename from scripts/check-architecture.py rename to scripts/check-functional-core.py diff --git a/scripts/check-host.sh b/scripts/check-host.sh deleted file mode 100755 index 3b122b0..0000000 --- a/scripts/check-host.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh -set -eu - -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -cd "$ROOT" - -case "$(uname -s)" in - Darwin|Linux) ;; - *) - echo "Gitveil local checks support macOS and Linux only" >&2 - exit 2 - ;; -esac - -if ! cargo nextest --version >/dev/null 2>&1; then - echo "cargo-nextest is required: cargo install cargo-nextest --locked" >&2 - exit 2 -fi - -SOPS_TEST_BIN=$(python3 scripts/fetch-test-sops.py) -AGE_KEYGEN_TEST_BIN=$(python3 scripts/fetch-test-age.py) -AGE_KEYGEN_TEST_ARCHIVE=$(python3 scripts/fetch-test-age.py --print-archive) -cargo fmt --all -- --check -python3 scripts/check-architecture.py -python3 scripts/check-docs.py -cargo clippy --all-targets --all-features --locked -- -D warnings -AGE_KEYGEN_BIN="$AGE_KEYGEN_TEST_BIN" cargo nextest run --all-features --locked -cargo deny check -cargo build --release --locked -python3 scripts/package-release.py \ - --skip-build \ - --binary target/release/gitveil \ - --sops-bin "$SOPS_TEST_BIN" \ - --age-keygen-bin "$AGE_KEYGEN_TEST_BIN" \ - --age-keygen-archive "$AGE_KEYGEN_TEST_ARCHIVE" \ - --output-dir target/package-test -git diff --check diff --git a/scripts/check-linux.sh b/scripts/check-linux.sh deleted file mode 100755 index fb21805..0000000 --- a/scripts/check-linux.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/sh -set -eu - -ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -RUST_TOOLCHAIN=$(sed -n 's/^channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$ROOT/rust-toolchain.toml") -if [ -z "$RUST_TOOLCHAIN" ]; then - echo "Unable to read the pinned toolchain from rust-toolchain.toml" >&2 - exit 2 -fi -SOPS_VERSION=$(python3 "$ROOT/scripts/sops_artifacts.py" --print-version) -SOPS_BASELINE_VERSION=$(python3 "$ROOT/scripts/sops_artifacts.py" --print-version --baseline) -AGE_VERSION=$(python3 "$ROOT/scripts/age_artifacts.py" --print-version) -IMAGE_KEY=$(cat "$ROOT/docker/test.Dockerfile" "$ROOT/scripts/sops_artifacts.py" "$ROOT/scripts/age_artifacts.py" "$ROOT/rust-toolchain.toml" | cksum | awk '{ print $1 "-" $2 }') -IMAGE=${GITVEIL_LINUX_TEST_IMAGE:-gitveil-test:rust-$RUST_TOOLCHAIN-sops-$SOPS_VERSION-age-$AGE_VERSION-$IMAGE_KEY} -SOPS_METADATA=$(python3 "$ROOT/scripts/sops_artifacts.py" --system linux --machine "$(uname -m)") -SOPS_ARTIFACT=$(printf '%s\n' "$SOPS_METADATA" | cut -f1) -SOPS_SHA256=$(printf '%s\n' "$SOPS_METADATA" | cut -f2) -SOPS_BASELINE_METADATA=$(python3 "$ROOT/scripts/sops_artifacts.py" --baseline --system linux --machine "$(uname -m)") -SOPS_BASELINE_ARTIFACT=$(printf '%s\n' "$SOPS_BASELINE_METADATA" | cut -f1) -SOPS_BASELINE_SHA256=$(printf '%s\n' "$SOPS_BASELINE_METADATA" | cut -f2) -AGE_METADATA=$(python3 "$ROOT/scripts/age_artifacts.py" --system linux --machine "$(uname -m)") -AGE_ARTIFACT=$(printf '%s\n' "$AGE_METADATA" | cut -f1) -AGE_SHA256=$(printf '%s\n' "$AGE_METADATA" | cut -f2) - -if ! command -v docker >/dev/null 2>&1; then - echo "Docker is required for local Linux tests" >&2 - exit 2 -fi - -if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then - printf 'Building the local Linux test image; the first run downloads the Rust %s base image.\n' "$RUST_TOOLCHAIN" - docker build \ - --file "$ROOT/docker/test.Dockerfile" \ - --build-arg "RUST_TOOLCHAIN=$RUST_TOOLCHAIN" \ - --build-arg "SOPS_VERSION=$SOPS_VERSION" \ - --build-arg "SOPS_ARTIFACT=$SOPS_ARTIFACT" \ - --build-arg "SOPS_SHA256=$SOPS_SHA256" \ - --build-arg "SOPS_BASELINE_VERSION=$SOPS_BASELINE_VERSION" \ - --build-arg "SOPS_BASELINE_ARTIFACT=$SOPS_BASELINE_ARTIFACT" \ - --build-arg "SOPS_BASELINE_SHA256=$SOPS_BASELINE_SHA256" \ - --build-arg "AGE_VERSION=$AGE_VERSION" \ - --build-arg "AGE_ARTIFACT=$AGE_ARTIFACT" \ - --build-arg "AGE_SHA256=$AGE_SHA256" \ - --tag "$IMAGE" \ - "$ROOT/docker" -else - printf 'Using cached local Linux test image %s\n' "$IMAGE" -fi - -docker run --rm \ - --mount "type=bind,source=$ROOT,target=/source,readonly" \ - --mount "type=volume,source=gitveil-cargo-registry,target=/usr/local/cargo/registry" \ - "$IMAGE" \ - sh -eu -c ' - mkdir -p /work - tar --exclude=.git --exclude=target -C /source -cf - . | tar -C /work -xf - - cd /work - python3 scripts/check-architecture.py - python3 scripts/check-docs.py - cargo clippy --all-targets --all-features --locked -- -D warnings - cargo nextest run --all-features --locked - cargo build --release --locked - python3 scripts/package-release.py \ - --skip-build \ - --binary target/release/gitveil \ - --sops-bin /usr/local/bin/sops \ - --age-keygen-bin /usr/local/bin/age-keygen \ - --age-keygen-archive /usr/local/share/gitveil-test/age.tar.gz \ - --output-dir target/package-test - ' diff --git a/scripts/fetch-test-age.py b/scripts/fetch-test-age-keygen.py similarity index 92% rename from scripts/fetch-test-age.py rename to scripts/fetch-test-age-keygen.py index fa15565..ce4ec9b 100755 --- a/scripts/fetch-test-age.py +++ b/scripts/fetch-test-age-keygen.py @@ -7,7 +7,7 @@ from pathlib import Path import sys -from age_artifacts import VERSION, artifact_for, fetch_verified +from _lib.age_keygen_artifacts import VERSION, artifact_for, fetch_verified def main() -> int: diff --git a/scripts/fetch-test-sops.py b/scripts/fetch-test-sops.py index 2c2c147..89bade8 100755 --- a/scripts/fetch-test-sops.py +++ b/scripts/fetch-test-sops.py @@ -12,7 +12,7 @@ from pathlib import Path import sys -from sops_artifacts import ( +from _lib.sops_artifacts import ( COMPATIBILITY_BASELINE_VERSION, VERSION, artifact_for, diff --git a/scripts/install-local.py b/scripts/install-from-source.py similarity index 98% rename from scripts/install-local.py rename to scripts/install-from-source.py index 5b50915..6a3c14b 100755 --- a/scripts/install-local.py +++ b/scripts/install-from-source.py @@ -12,7 +12,12 @@ import tarfile import tempfile -from release_package import archive_root_name, build_archive, package_version, validate_archive +from _lib.release_archive import ( + archive_root_name, + build_archive, + package_version, + validate_archive, +) _INSTALL_MEMBERS = ( ("share/licenses/gitveil/LICENSE", 0o644), diff --git a/tests/package_contract.rs b/tests/package_contract.rs index b9b385a..e3aab0f 100644 --- a/tests/package_contract.rs +++ b/tests/package_contract.rs @@ -10,12 +10,11 @@ use support::{ command_output, sops_binary, }; -fn installer_command() -> Command { +fn source_installer_command() -> Command { let root = Path::new(env!("CARGO_MANIFEST_DIR")); - let mut command = Command::new("python3"); + let mut command = Command::new(root.join("scripts/install-from-source.py")); command .current_dir(root) - .arg("scripts/install-local.py") .arg("--skip-build") .arg("--binary") .arg(assert_cmd::cargo::cargo_bin!("gitveil")) @@ -127,33 +126,33 @@ fn assert_installed_layout(prefix: &Path) { } #[test] -fn local_installer_defaults_to_cargo_home_and_installs_the_complete_layout() { +fn source_installer_defaults_to_cargo_home_and_installs_the_complete_layout() { let temporary = tempfile::tempdir().expect("installer fixture"); let cargo_home = temporary.path().join("cargo-home"); let output = command_output( - installer_command() + source_installer_command() .env("HOME", temporary.path()) .env("CARGO_HOME", &cargo_home), ); - assert_success(output, "default local install"); + assert_success(output, "default source install"); assert_installed_layout(&cargo_home); } #[test] -fn local_installer_honors_a_custom_prefix_and_can_replace_an_existing_install() { +fn source_installer_honors_a_custom_prefix_and_can_replace_an_existing_install() { let temporary = tempfile::tempdir().expect("installer fixture"); let cargo_home = temporary.path().join("unused-cargo-home"); let prefix = temporary.path().join("custom-prefix"); for _ in 0..2 { let output = command_output( - installer_command() + source_installer_command() .env("HOME", temporary.path()) .env("CARGO_HOME", &cargo_home) .arg("--prefix") .arg(&prefix), ); - assert_success(output, "custom local install"); + assert_success(output, "custom source install"); } assert_installed_layout(&prefix); @@ -164,7 +163,7 @@ fn local_installer_honors_a_custom_prefix_and_can_replace_an_existing_install() } #[test] -fn local_installer_rejects_a_directory_destination_without_partial_replacement() { +fn source_installer_rejects_a_directory_destination_without_partial_replacement() { let temporary = tempfile::tempdir().expect("installer fixture"); let prefix = temporary.path().join("prefix"); let existing_sidecar = prefix.join("libexec/gitveil/sops"); @@ -174,7 +173,7 @@ fn local_installer_rejects_a_directory_destination_without_partial_replacement() fs::create_dir_all(prefix.join("bin/gitveil")).expect("create conflicting directory"); let output = command_output( - installer_command() + source_installer_command() .env("HOME", temporary.path()) .arg("--prefix") .arg(&prefix), @@ -207,7 +206,7 @@ fn release_packaging_rejects_a_sidecar_with_the_wrong_checksum_without_an_archiv Command::new("python3") .current_dir(root) .env("PYTHONDONTWRITEBYTECODE", "1") - .arg("scripts/package-release.py") + .arg("scripts/build-release-archive.py") .arg("--skip-build") .arg("--binary") .arg(assert_cmd::cargo::cargo_bin!("gitveil")) @@ -244,7 +243,7 @@ fn release_packaging_rejects_a_supplied_age_keygen_without_its_verified_archive( .current_dir(root) .env("PYTHONDONTWRITEBYTECODE", "1") .env("https_proxy", "http://127.0.0.1:9") - .arg("scripts/package-release.py") + .arg("scripts/build-release-archive.py") .arg("--skip-build") .arg("--binary") .arg(assert_cmd::cargo::cargo_bin!("gitveil")) @@ -275,7 +274,7 @@ fn release_packaging_rejects_an_age_keygen_with_the_wrong_checksum() { Command::new("python3") .current_dir(root) .env("PYTHONDONTWRITEBYTECODE", "1") - .arg("scripts/package-release.py") + .arg("scripts/build-release-archive.py") .arg("--skip-build") .arg("--binary") .arg(assert_cmd::cargo::cargo_bin!("gitveil")) @@ -312,7 +311,7 @@ fn release_packaging_rejects_an_unverified_age_archive() { .current_dir(root) .env("PYTHONDONTWRITEBYTECODE", "1") .env("https_proxy", "http://127.0.0.1:9") - .arg("scripts/package-release.py") + .arg("scripts/build-release-archive.py") .arg("--skip-build") .arg("--binary") .arg(assert_cmd::cargo::cargo_bin!("gitveil")) diff --git a/tests/support/mod.rs b/tests/support/mod.rs index aaf0cec..94ec72a 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -14,7 +14,7 @@ const PROCESS_TIMEOUT: Duration = Duration::from_secs(30); /// The SOPS build that wrote ciphertext already living in user repositories. /// -/// Mirrors `COMPATIBILITY_BASELINE_VERSION` in `scripts/sops_artifacts.py`; the two +/// Mirrors `COMPATIBILITY_BASELINE_VERSION` in `scripts/_lib/sops_artifacts.py`; the two /// are held in agreement by `baseline_binary_is_exactly_the_declared_baseline`, which /// executes the fetched binary rather than trusting either declaration. pub const SOPS_COMPATIBILITY_BASELINE: (u64, u64, u64) = (3, 13, 2);