diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 222d63f..1d58136 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,7 +12,7 @@ body: attributes: label: linux-context version description: Output of `./bin/linux-context --version`; for checkouts also include `git rev-parse HEAD`. Unreleased checkouts can share a version string with a release. - placeholder: linux-context 1.0.0 + placeholder: linux-context 1.1.0 validations: required: true - type: dropdown diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..78f1929 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,145 @@ +name: Release + +# Only successful CI of trusted main pushes may publish. PR runs never qualify. +on: + workflow_run: + workflows: [CI] + types: [completed] + branches: [main] + +permissions: + contents: read + +concurrency: + group: release-main + cancel-in-progress: false + +jobs: + publish: + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.head_repository.full_name == github.repository + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: write + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_COMMIT: ${{ github.event.workflow_run.head_sha }} + steps: + - name: Check out the exact successful CI commit + uses: actions/checkout@v7 + with: + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + persist-credentials: false + - name: Decide whether this version needs publication + id: version + shell: bash + run: | + set -euo pipefail + [[ "$(git rev-parse HEAD)" == "$RELEASE_COMMIT" ]] + current=$(gh api "repos/$GH_REPO/commits/main" --jq .sha) + if [[ "$current" != "$RELEASE_COMMIT" ]]; then + echo 'Main advanced; wait for CI of its latest commit.' + exit 0 + fi + version=$(cat VERSION) + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] + tag="v$version" + gh api --paginate "repos/$GH_REPO/releases" \ + --jq ".[] | select(.tag_name == \"$tag\")" > "$RUNNER_TEMP/existing-release.json" + resume=false + if [[ -s "$RUNNER_TEMP/existing-release.json" ]]; then + if [[ "$(jq -r .draft "$RUNNER_TEMP/existing-release.json")" == false ]]; then + echo "$tag is already published; nothing to do." + exit 0 + fi + [[ "$(jq -r .target_commitish "$RUNNER_TEMP/existing-release.json")" == "$RELEASE_COMMIT" ]] || { + echo 'Existing draft targets a different commit; refusing replacement.' >&2 + exit 1 + } + resume=true + fi + if git rev-parse --verify "refs/tags/$tag" >/dev/null 2>&1; then + [[ "$(git rev-parse "$tag^{commit}")" == "$RELEASE_COMMIT" ]] || { + echo 'Existing tag points elsewhere; refusing to retag.' >&2 + exit 1 + } + fi + printf 'ready=true\nversion=%s\ntag=%s\nresume=%s\n' "$version" "$tag" "$resume" >> "$GITHUB_OUTPUT" + - name: Build and verify the committed source archive + if: steps.version.outputs.ready == 'true' + env: + RELEASE_VERSION: ${{ steps.version.outputs.version }} + shell: bash + run: | + set -euo pipefail + bash scripts/build-release.sh "$RUNNER_TEMP/release" + mkdir "$RUNNER_TEMP/extracted" + tar -xzf "$RUNNER_TEMP/release/linux-context-v$RELEASE_VERSION.tar.gz" -C "$RUNNER_TEMP/extracted" + cd "$RUNNER_TEMP/extracted/linux-context-v$RELEASE_VERSION" + [[ "$(./bin/linux-context --version)" == "linux-context $RELEASE_VERSION" ]] + make check + bash tests/smoke-distribution.sh + - name: Prepare release notes + if: steps.version.outputs.ready == 'true' + env: + RELEASE_VERSION: ${{ steps.version.outputs.version }} + shell: bash + run: | + python3 -B -S - <<'PY' + import os, re + from pathlib import Path + version = os.environ['RELEASE_VERSION'] + text = Path('CHANGELOG.md').read_text() + match = re.search(r'^## ' + re.escape(version) + r' - [^\n]+\n(.*?)(?=^## |\Z)', text, re.M | re.S) + if not match: + raise SystemExit('Release section is missing from CHANGELOG.md') + temp = Path(os.environ['RUNNER_TEMP']) + checksum = (temp / 'release' / f'linux-context-v{version}.tar.gz.sha256').read_text().strip() + notes = f'# linux-context v{version}\n\n' + match.group(1).strip() + '\n\n' + notes += '## Download and verification\n\nThe attached source archive is built from commit `' + os.environ['RELEASE_COMMIT'] + '`, tested after extraction, and preserves executable script modes.\n\n' + notes += 'SHA-256:\n\n```text\n' + checksum + '\n```\n\n' + notes += '```bash\nsha256sum -c linux-context-v' + version + '.tar.gz.sha256\ntar -xzf linux-context-v' + version + '.tar.gz\ncd linux-context-v' + version + '\n./bin/linux-context --version\nsudo ./bin/linux-context --profile max\n```\n\nSend `context.ai.json` first; retrieve graph/evidence only as needed. Review diagnostic data before sharing.\n' + notes += '\n[Full changelog](https://github.com/' + os.environ['GH_REPO'] + '/blob/v' + version + '/CHANGELOG.md)\n' + (temp / 'release-notes.md').write_text(notes) + PY + - name: Upload draft assets and verify their downloaded bytes + if: steps.version.outputs.ready == 'true' + env: + RELEASE_TAG: ${{ steps.version.outputs.tag }} + RELEASE_VERSION: ${{ steps.version.outputs.version }} + RESUME_DRAFT: ${{ steps.version.outputs.resume }} + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/release/linux-context-v$RELEASE_VERSION.tar.gz" + if [[ "$RESUME_DRAFT" == true ]]; then + gh release upload "$RELEASE_TAG" "$archive" "$archive.sha256" --clobber + gh release edit "$RELEASE_TAG" --notes-file "$RUNNER_TEMP/release-notes.md" + else + gh release create "$RELEASE_TAG" "$archive" "$archive.sha256" \ + --draft --target "$RELEASE_COMMIT" --title "linux-context $RELEASE_TAG" \ + --notes-file "$RUNNER_TEMP/release-notes.md" + fi + mkdir -p "$RUNNER_TEMP/downloaded" + gh release download "$RELEASE_TAG" --pattern 'linux-context-*.tar.gz*' --dir "$RUNNER_TEMP/downloaded" + (cd "$RUNNER_TEMP/downloaded" && sha256sum -c "linux-context-v$RELEASE_VERSION.tar.gz.sha256") + cmp "$archive" "$RUNNER_TEMP/downloaded/linux-context-v$RELEASE_VERSION.tar.gz" + cmp "$archive.sha256" "$RUNNER_TEMP/downloaded/linux-context-v$RELEASE_VERSION.tar.gz.sha256" + - name: Publish the verified release + if: steps.version.outputs.ready == 'true' + env: + RELEASE_TAG: ${{ steps.version.outputs.tag }} + shell: bash + run: | + set -euo pipefail + current=$(gh api "repos/$GH_REPO/commits/main" --jq .sha) + [[ "$current" == "$RELEASE_COMMIT" ]] || { echo 'Main advanced before publication; draft retained.' >&2; exit 1; } + gh release edit "$RELEASE_TAG" --draft=false --latest + [[ "$(gh api "repos/$GH_REPO/commits/$RELEASE_TAG" --jq .sha)" == "$RELEASE_COMMIT" ]] + gh release view "$RELEASE_TAG" --json url,isDraft,isPrerelease diff --git a/CHANGELOG.md b/CHANGELOG.md index fd09777..8477219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,63 +2,76 @@ ## Unreleased -Unreleased entries describe changes after the 1.0.0 release, even while the -checkout's `VERSION` remains 1.0.0. Older sections describe historical behavior, -not the current acquisition/format contract. - -### Documentation - -- Align user, format, collector, contribution and security guides with current - main; clarify AI-first retrieval, baseline/target selection, byte-budget scope, - incomplete coverage and directory/archive failure boundaries. -- Add complete collector/profile/API tables and a troubleshooting guide, - including the remaining BusyBox setpriv limitation in optional owner probes. -- Preserve historical audit measurements with exact revision and CI references; - distinguish tool/schema/encoding versions and checkout changes from releases. - -### Portability, correctness and performance - -- Add lossless AI encoding v2: endpoint references, label/provenance defaults and - pooled observations; select the smallest of canonical/v1/v2 encodings and keep - backward-compatible decoding. The supplied rich sample shrinks from 114,586 - canonical bytes (96,592 old AI bytes) to 74,134 bytes without dropping data. -- Cache collector metadata and initialize stages/hash manifests in single Python - passes; batch selected sysctls, reuse package inventories for counts, and share - one interpreter across the final redaction/validation/AI/scan/manifest gates. -- Use sysfs adapters for automatic Bluetooth discovery; reserve bounded BlueZ - controller queries for explicit targets (three-second discovery limit). -- Add Python timeout and owner-handoff fallbacks for minimal/BusyBox userlands; - bound detection, collector execution and previously unbounded daemon calls. -- Read vendor os-release fallback safely, expose PID 1, gate systemd runtime, - suppress container reboot guesses, and distinguish installed network clients - from running managers and failed LSM queries from disabled protection. -- Filter residual Debian package records; avoid false zero/exact counts on - failed or truncated inventories; support portable filesystem/process/boot/cron - observations and preserve IPv6 routes and coexisting firewall state. -- Observe procfs truncation by actual captured bytes; retain caller shell flags; - reject missing worker status/invalid catalog/empty targets and reconcile final - redacted evidence/context byte budgets before publication. -- Add package-family, runtime/fallback, model/privacy, manager-state, final-budget - and randomized AI round-trip regressions plus four-distribution container CI. -- Accept IEC capacity units (GiB/MiB and related sizes) from newer dmidecode - output, restoring module/installed capacity and memory-array limits. -- Bound Bluetooth controller discovery with probe telemetry; - failed/truncated discovery leaves the controller count unknown instead of zero. -- Stop and reap collector process trees before private staging cleanup on INT/TERM - or early failure, including producers in GNU timeout's separate process groups. -- Add hanging/failed/empty/successful Bluetooth and INT/TERM cleanup regressions. -- Add standard-profile typed SMBIOS RAM arrays/modules with capacity, DDR type, - manufacturer/part number, slot/bank, rank, ECC, rated/configured speeds and - explicit missing-tool/access/firmware/truncation coverage. Keep serial/asset - identifiers out of acquisition output. -- Add typed CPU topology/cache/selected ISA features, firmware models, GPU PCI - name/driver/available VRAM, disk models/transport/capacity, system - battery full/design capacity health, available RAM and swap counters. -- Reduce duplicate hardware text in automatic snapshots; retain focused evidence - under named hardware targets and `all`, without changing the schema or budgets. -- Add a lossless dictionary-coded `context.ai.json` AI view with round-trip - validation, residual scanning and manifest/archive coverage. -- Add hardware parser/sysfs/privacy fixtures and memory-collector integration tests. +No changes yet. Unreleased entries describe changes after the latest tagged +release; older sections record historical behavior. + +## 1.1.0 - 2026-10-03 + +### Hardware identification + +- Add standard-tier typed RAM arrays/modules: capacity, DDR type, + manufacturer/part number, slots, ECC, rank and rated/configured speeds. + Missing tools/access/firmware, unknown sizes and truncated inventories stay + explicit. Parse IEC capacity units from newer dmidecode versions. +- Model CPU topology/cache/selected features, firmware, GPU driver/available + VRAM, disks and system battery full/design capacity. Omit module/device serial + identifiers and redundant automatic hardware command dumps; retain focused + evidence through named targets/all. + +### AI representation and performance + +- Add `context.ai.json`, with lossless v1/v2 dictionaries for graph names, + endpoints, labels, provenance and repeated observations. Select the smallest + canonical/v1/v2 representation; preserve types/conflicts and validate round trips. + Canonical schema v5 and older v1 decoding remain supported. +- Re-encoding the supplied rich sample reduced the previous AI file from 96,592 + to 74,134 bytes (23.3%); no data was dropped. This is a byte measurement, not a + tokenizer result or a universal size guarantee. +- Cache collector metadata, batch stage initialization/checksums/finalization, + reuse package inventories and batch selected sysctls. Five alternating local + before/after pairs reduced median quick/standard/max elapsed time by + 42.5%/17.7%/3.9%; hardware/daemon performance varies. See `docs/AUDIT.md`. +- Use sysfs Bluetooth adapter counts automatically; query BlueZ only for focused + bluetooth/workstation/all targets with a three-second discovery limit. Failed + or truncated discovery leaves controller counts unknown. + +### Portability and correctness + +- Add Python timeout/owner-handoff paths and portable statvfs/procfs/boot/cron + observations. Bound metadata/detection and collector acquisition, and stop + nested producer trees before cleanup on timeout, failure or INT/TERM. +- Safely parse vendor os-release fallback, expose PID 1, require systemd runtime, + distinguish clients from running managers, and keep failed protection queries + separate from disabled LSM state. Avoid container reboot guesses. +- Filter Debian residual package records; avoid false zero/exact counts on failed + or truncated inventories. Preserve IPv6 routes and coexisting firewall evidence. +- Detect procfs truncation from captured bytes, preserve caller shell flags, + reject malformed catalogs/missing worker status/empty targets, and reconcile + final sanitized evidence/context budgets and graph integrity before publication. + +### Documentation and release delivery + +- Rewrite and align user/consumer/developer/security guides with current code; + document the full 25-collector catalog, profile settings and collector API. + Add troubleshooting and update issue/PR templates. +- Add reproducible source packaging from the committed tree and SHA-256 assets. + Successful trusted main CI triggers release preparation; extracted archives + pass checks/smoke and uploaded draft bytes are verified before publication. + +### Validation and limits + +- 31 regression scripts cover package families, models, managers, privacy, + timeout/tree cleanup, final budgets and type-sensitive AI round trips. + CI covers Python 3.9/3.11/3.13 plus Debian 12, Fedora 43, Arch and Alpine 3.22, + including all-profile archive/manifest/AI/privacy smoke checks. +- Container CI does not establish every live hardware/init/desktop configuration. + Native Gentoo/Nix/Slackware package inventories remain outside current support. +- Optional per-user runner probes still select setpriv by presence and can fail + with BusyBox setpriv under sudo without runuser. Publication handoff has the + capability-aware Python fallback; see `docs/TROUBLESHOOTING.md` for the gap. +- Canonical/evidence byte ceilings do not cap full graph/metadata, transient + acquisition or total archive size. Review bundles before sharing; zero known + high-confidence residuals does not prove arbitrary content secret-free. ## 1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 235224d..06011bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -196,9 +196,22 @@ Keep historical changelog entries as history; use Unreleased for new behavior. `VERSION` is the tool version source. Do not infer a new release number from a schema bump or a merge. When preparing a release, intentionally update VERSION and current-version examples, move Unreleased entries to the release section and -validate the exact tagged tree. No automated release-packaging workflow currently -ships in this repository; published archive/checksum examples depend on the -maintainer supplying those files. Preserve executable script modes. +validate the exact tagged tree. Preserve executable script modes. + +`bash scripts/build-release.sh OUTPUT_DIR` creates the versioned source archive +and SHA-256 file from Git HEAD, not uncommitted files. Git and gzip/sha256sum are +build tools, not extra collection dependencies. Gzip timestamps are omitted for +repeatable bytes from the same committed tree. Existing output files are refused. + +The Release workflow runs only after successful CI of a trusted main push. It +checks out that exact commit, skips already published versions, refuses conflicting +tags/drafts, builds/tests the extracted archive and verifies downloaded draft +assets before publishing as latest. It never consumes PR artifacts or runs for +PR CI. Main must still point at the tested commit at publication. A failed +same-commit draft can be resumed by rerunning the successful main CI run; a draft +for another commit requires maintainer review. `contents: write` is scoped to the +release job. Version changes on main therefore request a stable release after CI; +do not bump VERSION just to label an unreleased development commit. PRs should explain the trigger, resulting behavior and validation/limits. Include version/commit, distribution and relevant tool variants for portability changes. diff --git a/Makefile b/Makefile index 5b63d88..900be0a 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ test: syntax: shell-check python-check shell-check: - @find bin collectors lib tests -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n + @find bin collectors lib tests scripts -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n @bash -n bin/linux-context python-check: diff --git a/README.md b/README.md index 195091d..fb7ed2d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ without making a large command dump the initial prompt. Collection inspects the machine; it does not perform remediation. Diagnostic data can still be sensitive, so review a bundle before sharing it. -The checkout's version is defined by [`VERSION`](VERSION), currently `1.0.0`. +The checkout's version is defined by [`VERSION`](VERSION), currently `1.1.0`. Changes under [Unreleased](CHANGELOG.md#unreleased) may be newer than a tagged release with the same version string. Include a Git commit when reporting a problem from a checkout. @@ -259,7 +259,7 @@ to identify the code you run. If a release provides a checksum file, verify it before extraction: ```bash -sha256sum -c linux-context-v1.0.0.tar.gz.sha256 +sha256sum -c linux-context-v1.1.0.tar.gz.sha256 ``` This example requires the named files from that release; no checksum file is diff --git a/VERSION b/VERSION index 3eefcb9..9084fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.1.0 diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index fbba38e..0e98e0c 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -16,7 +16,8 @@ From the source root: git rev-parse HEAD ``` -`VERSION` may still be `1.0.0` on a checkout containing newer Unreleased changes. +`VERSION` may match a tagged release (for example `1.1.0`) even when a checkout +contains newer Unreleased changes. List commands inspect metadata, not actual host support. The launcher expects Linux, Bash 4+, Python 3.9+, executable collectors, common utilities and archive tooling unless `--no-archive` is used. Use the entire trusted project directory, diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100755 index 0000000..704f8df --- /dev/null +++ b/scripts/build-release.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Archive the committed tree, never incidental working-directory contents. +set -euo pipefail +ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +output=${1:?Usage: build-release.sh OUTPUT_DIR} +version=$(git -C "$ROOT" show HEAD:VERSION) +[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { printf 'A stable semantic VERSION is required\n' >&2; exit 1; } +mkdir -p -- "$output" +output=$(cd -- "$output" && pwd) +base="linux-context-v$version" +archive="$output/$base.tar.gz" +checksum="$archive.sha256" +[[ ! -e "$archive" && ! -e "$checksum" ]] || { printf 'Release output already exists\n' >&2; exit 1; } +work=$(mktemp -d "$output/.release.XXXXXX") +trap 'rm -rf -- "$work"' EXIT +git -C "$ROOT" -c tar.umask=0022 archive --format=tar --prefix="$base/" HEAD | gzip -n > "$work/$base.tar.gz" +(cd "$work" && sha256sum "$base.tar.gz" > "$base.tar.gz.sha256" && sha256sum -c "$base.tar.gz.sha256") +mv -- "$work/$base.tar.gz" "$archive" +mv -- "$work/$base.tar.gz.sha256" "$checksum" +printf 'Archive: %s\nChecksum: %s\n' "$archive" "$checksum"