From a40c0e3575789969451a5a29093ea1b53f6af9e9 Mon Sep 17 00:00:00 2001 From: sammiller Date: Mon, 21 Sep 2026 11:55:43 -0700 Subject: [PATCH] =?UTF-8?q?[WP02=20=C2=B7=20SubStep=2002.04]=20Reduce=20CI?= =?UTF-8?q?=20to=20Windows/Linux=20builds=20and=20offline=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .githooks/pre-commit | 1 - .githooks/pre-push | 6 +- .github/workflows/ci.yml | 56 ++++++------------- AGENTS.md | 11 +++- CONTRIBUTING.md | 9 ++- README.md | 9 ++- docs/bootstrap-plan.md | 2 + docs/build-identity.md | 14 +++-- docs/development.md | 18 ++++-- docs/licence-boundary.md | 13 +++-- docs/provenance.md | 12 ++-- docs/release-notes.md | 8 +-- docs/releasing.md | 26 ++++----- eng/ArcForges.Repository/Program.cs | 44 +++++---------- .../CandidateTests.cs | 24 ++------ 15 files changed, 103 insertions(+), 150 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index e79f516..34f51c4 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,4 +1,3 @@ #!/bin/sh set -eu -dotnet run --project eng/ArcForges.Repository -- check git diff --cached --check diff --git a/.githooks/pre-push b/.githooks/pre-push index 60abe87..9569ade 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,6 +1,4 @@ #!/bin/sh set -eu -dotnet restore ArcNotes.slnx --locked-mode -dotnet format ArcNotes.slnx --verify-no-changes --no-restore -dotnet build ArcNotes.slnx -c Release --no-restore -dotnet test --project tests/ArcForges.ArcNotes.Tests/ArcForges.ArcNotes.Tests.csproj -c Release --no-build +# Build and test explicitly once when the change requires them; never on every push. +git diff --check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 939ce62..3394928 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,10 +34,6 @@ jobs: rid: win-arm64 - os: ubuntu-24.04 rid: linux-x64 - - os: macos-15-intel - rid: osx-x64 - - os: macos-15 - rid: osx-arm64 defaults: run: shell: bash @@ -53,17 +49,24 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y clang zlib1g-dev libx11-6 libice6 libsm6 libfontconfig1 xvfb fonts-dejavu-core + sudo apt-get install -y clang zlib1g-dev - run: dotnet restore ArcNotes.slnx --locked-mode - - run: dotnet format ArcNotes.slnx --verify-no-changes --no-restore + - name: Check formatting once + if: runner.os == 'Linux' + run: dotnet format ArcNotes.slnx --verify-no-changes --no-restore - run: dotnet build ArcNotes.slnx -c Release --no-restore - - run: dotnet run --project eng/ArcForges.Repository -c Release --no-build -- verify-assemblies - - run: dotnet run --project eng/ArcForges.Repository -c Release --no-build -- check + - name: Run verify-assemblies once + if: runner.os == 'Linux' + run: dotnet run --project eng/ArcForges.Repository -c Release --no-build -- verify-assemblies + - name: Run check once + if: runner.os == 'Linux' + run: dotnet run --project eng/ArcForges.Repository -c Release --no-build -- check - name: Set immutable version id: version run: dotnet run --project eng/ArcForges.Repository -c Release --no-build -- version - - name: Run deterministic tests - run: dotnet test --project tests/ArcForges.ArcNotes.Tests/ArcForges.ArcNotes.Tests.csproj -c Release --no-build + - name: Run offline unit tests once + if: runner.os == 'Linux' + run: dotnet test --project tests/ArcForges.ArcNotes.Tests/ArcForges.ArcNotes.Tests.csproj -c Release --no-build --filter-not-class '*.TransportTests' - name: Compile Native AOT executable env: PACKAGE_VERSION: ${{ steps.version.outputs.version }} @@ -74,16 +77,7 @@ jobs: PACKAGE_VERSION: ${{ steps.version.outputs.version }} TARGET_RID: ${{ matrix.rid }} run: dotnet run --project eng/ArcForges.Repository -c Release --no-build -- prepare "$TARGET_RID" "$PACKAGE_VERSION" - - name: Run native UI and live Cloud checks - env: - TARGET_RID: ${{ matrix.rid }} - run: | - if [ "$RUNNER_OS" = "Linux" ]; then - xvfb-run -a dotnet run --project eng/ArcForges.Repository -c Release --no-build -- smoke "$TARGET_RID" - else - dotnet run --project eng/ArcForges.Repository -c Release --no-build -- smoke "$TARGET_RID" - fi - - name: Pack the verified native directory + - name: Pack the compiled native directory env: PACKAGE_VERSION: ${{ steps.version.outputs.version }} TARGET_RID: ${{ matrix.rid }} @@ -191,24 +185,6 @@ jobs: test "$QUALITY" = success test "$CODEQL" = success if [ "$EVENT" = pull_request ]; then test "$DEPENDENCIES" = success; else test "$DEPENDENCIES" = skipped; fi - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - persist-credentials: false - - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 - with: - global-json-file: global.json - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - pattern: arcnotes-candidate-*-${{ github.run_id }}-${{ github.run_attempt }} - path: artifacts/download - - run: dotnet restore eng/ArcForges.Repository --locked-mode - - run: dotnet build eng/ArcForges.Repository -c Release --no-restore - - id: version - run: dotnet run --project eng/ArcForges.Repository -c Release --no-build -- version - - name: Rehearse the complete release verification before merge - env: - PACKAGE_VERSION: ${{ steps.version.outputs.version }} - run: dotnet run --project eng/ArcForges.Repository -c Release --no-build -- verify artifacts/download "$PACKAGE_VERSION" "$GITHUB_SHA" release: name: Publish portable release @@ -237,7 +213,7 @@ jobs: - run: dotnet build eng/ArcForges.Repository -c Release --no-restore - id: version run: dotnet run --project eng/ArcForges.Repository -c Release --no-build -- version - - name: Verify immutable candidates + - name: Check candidate integrity once at publication handoff env: PACKAGE_VERSION: ${{ steps.version.outputs.version }} run: dotnet run --project eng/ArcForges.Repository -c Release --no-build -- verify artifacts/download "$PACKAGE_VERSION" "$GITHUB_SHA" @@ -250,7 +226,7 @@ jobs: tag="v$PACKAGE_VERSION" gh release create "$tag" --target "$GITHUB_SHA" --title "ArcNotes $PACKAGE_VERSION" --draft --prerelease --notes-file docs/release-notes.md mapfile -d '' assets < <(find artifacts/download -type f \( -name '*.zip' -o -name '*.tar.gz' -o -name '*.sha256' \) -print0) - test "${#assets[@]}" = 10 + test "${#assets[@]}" = 6 gh release upload "$tag" "${assets[@]}" tar --exclude='*.zip' --exclude='*.tar.gz' --exclude='*.sha256' -czf artifacts/arcnotes-verification.tar.gz -C artifacts/download . gh release upload "$tag" artifacts/arcnotes-verification.tar.gz diff --git a/AGENTS.md b/AGENTS.md index 5f5ef7c..ff303bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,11 @@ - Keep this repository a C# desktop consumer. Use published, exact NuGet dependencies and committed locks. Do not add sibling source references or submodules. - Keep AGPL-3.0-only headers and the existing licence. Do not import reference code under incompatible terms. - Preserve Native AOT, normal TLS validation, `/api` routing, RPC deadlines, cancellation and user-visible failures. No fake success, credential in a desktop binary, blanket trimming suppression or automatic application retries. -- Keep the production entry point offline until a user action. Live smoke testing is explicit and must exercise the native UI action and published client. -- Run the checks in `CONTRIBUTING.md`. Never describe a local test, cross-compile or mock as a hosted native or live service test. -- PRs validate; only successful main pushes publish all five verified candidates. Do not merge a PR unless the user requests it. +- Keep the production entry point offline until a user action. Runtime tests are explicit local opt-in only when the affected behavior needs them and the existing environment supports them. +- Follow the [accepted CI and local validation policy](https://github.com/ArcForges/ArcForges-Design/blob/47db6670a727317939b91245e8c0b288834acf99/docs/assurance/ci-and-local-validation-policy.md). No macOS CI, device/emulator/GUI/browser E2E, live service or installed-package consumer execution is permitted in any hosted workflow or nested default build/publish command. +- CI compiles and packages Windows x64/ARM64 and Linux x64. Run platform-independent static checks and offline unit tests once. Preserve dependency locks, necessary signing, source/licence provenance and one candidate integrity check at the publication handoff. +- Do not routinely download published assets, compare their hashes/members or run another installation/runtime verification cycle. A concrete integrity defect or explicit user request is required for a scoped diagnostic download. +- Validate only affected behavior once. Do not install or reinstall vcpkg, SDKs, emulators or toolchains to expand coverage. Hooks must not rebuild or test implicitly. Serialize CPU-heavy local work and reuse existing caches. +- Never describe unrun local/macOS/runtime checks as passed. Historical evidence is not a command to repeat it. Documentation-only changes need consistency review, not product builds. +- Use the normal network; no proxy 7890, other proxy configuration, wsl.exe or WSL wrappers. Stop and report the exact failing operation on a network failure without retries. +- Use retained worktrees/branches and PR titles prefixed with the current work package/substep. Review the full latest PR head and merge only after applicable reduced CI succeeds when merging is user-authorized. Post-merge checks stop after expected commit, required publication result and clean primary fast-forward; do not begin another validation cycle. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d65f5d1..160a996 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,17 +4,16 @@ Use a branch/worktree and open a pull request. Keep one coherent change per PR a ```sh dotnet restore ArcNotes.slnx --locked-mode -dotnet run --project eng/ArcForges.Repository -- hooks -dotnet run --project eng/ArcForges.Repository -- check dotnet format ArcNotes.slnx --verify-no-changes --no-restore dotnet build ArcNotes.slnx -c Release --no-restore -dotnet test --project tests/ArcForges.ArcNotes.Tests/ArcForges.ArcNotes.Tests.csproj -c Release --no-build +dotnet run --project eng/ArcForges.Repository -c Release --no-build -- check +dotnet test --project tests/ArcForges.ArcNotes.Tests/ArcForges.ArcNotes.Tests.csproj -c Release --no-build --filter-not-class '*.TransportTests' ``` -`dotnet format ArcNotes.slnx --no-restore` fixes C# formatting. Hooks are local to the checkout using Git worktree configuration. Pre-commit checks text and whitespace; pre-push restores locked dependencies, builds and runs deterministic tests. Hosted CI is authoritative even if hooks are not installed. Hooks do not call the live backend. +`dotnet format ArcNotes.slnx --no-restore` fixes C# formatting. Hooks are local to the checkout using Git worktree configuration. Pre-commit and pre-push check Git whitespace only. Run the relevant commands above explicitly once for source changes; documentation-only changes need consistency review. CI repeats no GUI/live/runtime scenarios and runs offline unit checks on one Linux host. Change direct dependency versions only in `Directory.Packages.props`. After an intentional dependency change, run `dotnet restore ArcNotes.slnx --force-evaluate -p:RestoreLockedMode=false`, inspect every lock diff, and rerun locked restore and validation. Updating an SDK also requires refreshing implicit runtime/compiler entries in locks. Do not hand-edit content hashes or disable audit/locked restore to make a bot PR green. Review upstream licence changes when updating dependencies. `third-party/` preserves primary licence texts missing from NuGet packages, with pinned source commits and hashes. Update those records if the corresponding upstream terms change; preserve their bytes. Native package notices embedded by upstream are collected automatically during staging. -Native release/live validation is described in [development](docs/development.md). No secrets are required for Hello. Do not place credentials or personal data in fixtures, logs or screenshots. Report security issues privately using [SECURITY](SECURITY.md). +Optional local native and loopback transport validation is described in [development](docs/development.md); it is not a CI or publication prerequisite. No secrets are required for Hello. Do not place credentials or personal data in fixtures, logs or screenshots. Report security issues privately using [SECURITY](SECURITY.md). diff --git a/README.md b/README.md index b131521..8fbda9d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Install .NET SDK **10.0.401** and the native build prerequisites in [development ```sh dotnet restore ArcNotes.slnx --locked-mode dotnet build ArcNotes.slnx -c Release --no-restore -dotnet test --project tests/ArcForges.ArcNotes.Tests/ArcForges.ArcNotes.Tests.csproj -c Release --no-build +dotnet test --project tests/ArcForges.ArcNotes.Tests/ArcForges.ArcNotes.Tests.csproj -c Release --no-build --filter-not-class '*.TransportTests' dotnet run --project src/ArcForges.ArcNotes dotnet run --project eng/ArcForges.Repository -- hooks ``` @@ -20,17 +20,16 @@ The UI, application state and repository tool are C#. Avalonia/Skia supply packa ## Downloads and automation -Each successful main push publishes a prerelease `v0.1.0-ci..` to [GitHub Releases](https://github.com/ArcForges/ArcNotes/releases). PRs build and test the same five Native AOT targets but never publish a release: +Each successful main push publishes a prerelease `v0.1.0-ci..` to [GitHub Releases](https://github.com/ArcForges/ArcNotes/releases). PRs compile the same three Windows/Linux Native AOT targets and run offline checks but never publish a release: | Platform | Archive | | --- | --- | | Windows x64 / ARM64 | Portable ZIP; extract everything and run `ArcNotes.exe` | | Linux x64 | Portable tar.gz; extract everything and run `./ArcNotes` in an X11/XWayland desktop | -| macOS Intel / Apple Silicon | tar.gz containing `ArcNotes.app` and notices | -Keep all files together. These self-contained builds do not require a .NET installation. They are development distributions: Windows binaries are unsigned and macOS bundles are ad-hoc signed, not Developer ID signed or notarized. No installer, app-store identity or OS trust claim is included. Linux system libraries are listed in [development](docs/development.md). Source for a release is its exact Git tag/commit. +Keep all files together. These self-contained builds do not require a .NET installation. They are development distributions: Windows binaries are unsigned. Automated releases do not contain macOS builds; local macOS source support remains available without CI or release claims. No installer, app-store identity or OS trust claim is included. Linux system libraries are listed in [development](docs/development.md). Source for a release is its exact Git tag/commit. -CI verifies the native window, UI action, live greeting, Unicode/boundaries, gRPC status and backend revision on each native host before packaging. `--smoke-live --evidence ` is an explicit network-using validation mode that closes the window afterward; it is not normal startup. See [release mechanics](docs/releasing.md) and [bootstrap plan/evidence](docs/bootstrap-plan.md). +CI performs compilation, packaging, offline unit/static checks and security scanning. It does not launch packaged applications, native UI or live Cloud requests. Runtime smoke is an explicit local-only command when needed; no post-publication asset download or runtime cycle is required. See [release mechanics](docs/releasing.md) and the [validation policy](https://github.com/ArcForges/ArcForges-Design/blob/47db6670a727317939b91245e8c0b288834acf99/docs/assurance/ci-and-local-validation-policy.md). ## Contribute and report issues diff --git a/docs/bootstrap-plan.md b/docs/bootstrap-plan.md index 8d9dd38..c9f237e 100644 --- a/docs/bootstrap-plan.md +++ b/docs/bootstrap-plan.md @@ -1,5 +1,7 @@ # ArcNotes desktop Hello bootstrap +> Historical bootstrap record. Its five-platform/live-test release procedure has been superseded by the [CI and local validation policy](https://github.com/ArcForges/ArcForges-Design/blob/47db6670a727317939b91245e8c0b288834acf99/docs/assurance/ci-and-local-validation-policy.md) and [current release mechanics](releasing.md). Retain the original account below as history; do not execute it as current instructions. + ## Source and bounded scope Copy the complete tracked ArcChat foundation at [merged commit 818ca61b106eb373de94622125a9a54e5a3644d5](https://github.com/ArcForges/ArcChat/commit/818ca61b106eb373de94622125a9a54e5a3644d5), then adapt product identity for ArcNotes (knowledge management). The target started at 9a9de616040622760aa1b8dd213d6310c9996539 with only the same AGPL licence. This is a Hello bootstrap, not implementation of the product's full business workflows. diff --git a/docs/build-identity.md b/docs/build-identity.md index be64463..45bd913 100644 --- a/docs/build-identity.md +++ b/docs/build-identity.md @@ -21,12 +21,14 @@ format, storage, capability, policy or extension version is invented for this foundation. Explicit absent states name their later responsible work packages. There is no first-party native ABI in this application. -Preparation executes the actual Native AOT candidate offline and compares its -report with independently obtained Git/run/release and restored dependency inputs. -The report is included in each portable archive. Archive verification reads it -and rejects even rehashed metadata changes. Existing native UI/live Cloud checks -remain required on all five native CI hosts. The `verify-assemblies` command reads -the actual PE metadata of the app, core, tests and repository tool after compilation. +The repository tool statically reads actual app/core/test/tool PE metadata once. +Preparation writes `build-identity.json` from the reviewed Git/run/release, restored +dependency and committed source inputs without launching the AOT app. This packaged +build-input receipt is not runtime execution evidence. The app's explicit local +`--build-info` command remains available for relevant support diagnostics. +Publication checks candidate identity and legal/source integrity once. CI has three +Windows/Linux compilation targets, no macOS/UI/live execution and no routine public +archive download or runtime verification cycle. Tests mutate each of the nine distinct source kinds, check deterministic output, and reject missing/unknown axes, aliases, duplicate subjects, malformed versions, diff --git a/docs/development.md b/docs/development.md index d2a4da1..0c6d8b4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -4,8 +4,8 @@ - .NET SDK 10.0.401; all build and test tools are C#/.NET. - Windows: Visual Studio C++ desktop build tools with the native host architecture toolchain and Windows SDK for Native AOT. -- Linux: an X11/XWayland desktop, `libx11-6 libice6 libsm6 libfontconfig1`; native compilation uses `clang zlib1g-dev`. CI also installs `xvfb fonts-dejavu-core` and uses a virtual display. -- macOS: Xcode command-line tools. Native hosts build and run their matching RID. The tool creates an `.app` and applies ad-hoc signing before the live test. +- Linux: an X11/XWayland desktop, `libx11-6 libice6 libsm6 libfontconfig1`; native compilation uses `clang zlib1g-dev`. CI installs only compilation prerequisites and never starts a display or application. +- macOS: Xcode command-line tools. Local source support only, with matching native tooling already installed. The tool can create an ad-hoc-signed `.app`; no macOS CI or automated release is produced. No service token, `.env` file or Cloudflare account is needed to consume public Hello. Production authentication will be a separately designed feature. Startup performs no network request. @@ -18,9 +18,9 @@ No service token, `.env` file or Cloudflare account is needed to consume public `GrpcChannel` ignores base URI paths. A delegating handler prepends `/api` exactly once to the generated service path. Binary gRPC-Web uses HTTP/1.1, normal TLS, a five-second RPC deadline and bounded message sizes. HTTP redirects and cookies are disabled. There is no automatic application retry. The backend may return `Unavailable` during service trouble or `ResourceExhausted` when a limit is reached; the user chooses when to retry. -## Native local test (Windows example) +## Optional local runtime test (Windows example) -After the source checks in `CONTRIBUTING.md`, use a fresh output directory and the actual 40-character commit SHA in place of `COMMIT`: +Only when a source change affects runtime behavior and the existing local environment supports it, use a fresh output directory and the actual 40-character commit SHA in place of `COMMIT`: ```sh dotnet publish src/ArcForges.ArcNotes/ArcForges.ArcNotes.csproj -c Release -r win-x64 --no-restore -p:Version=0.1.0-ci.0.1 -p:SourceRevisionId=COMMIT -o artifacts/publish/win-x64 @@ -32,3 +32,13 @@ dotnet run --project eng/ArcForges.Repository -c Release --no-build -- pack win- Use the matching RID on another native host; run the smoke command under `xvfb-run -a` on a headless Linux host. Staging/archives are not overwritten. Remove only the task's own `artifacts` directory before starting another local candidate. The explicit smoke starts the real Native AOT window, waits for Cloud health, fills the name field, invokes the window's button action, asserts the displayed live greeting and checks Unicode, maximum size and two application errors. It records a rendered window image, native AOT/RID/source/version and matching Cloud Worker/container revision. This is programmatic native UI validation, not a claim of manual interaction, accessibility, installer or trusted signing certification. Readiness polling does not retry a failed Hello RPC. + +The `smoke` command refuses CI execution. A normal prepare/pack/publish path never invokes it or starts the packaged app. Run an affected local scenario once; do not repeat it after publication. + +Loopback transport integration tests are also local opt-in: + +```sh +dotnet test --project tests/ArcForges.ArcNotes.Tests/ArcForges.ArcNotes.Tests.csproj -c Release --no-build --filter-class '*.TransportTests' +``` + +The default documented unit command excludes that class. Do not install extra platforms or tools to widen this workflow change's validation. diff --git a/docs/licence-boundary.md b/docs/licence-boundary.md index 311756d..ee62c11 100644 --- a/docs/licence-boundary.md +++ b/docs/licence-boundary.md @@ -14,14 +14,15 @@ from the owner's SPDX declaration; the existing distribution notices are retaine `Directory.Build.targets` also checks effective MSBuild properties before build and pack, so an imported or command-line override cannot bypass source checks. -The inventory test runs in every native CI job. It writes the source commit, +The inventory test runs once in the Linux CI job. It writes the source commit, dirty state, evaluated MSBuild declarations, reference edges and complete project list to -`artifacts/evidence/licence-boundary.json`, retained with the existing UI evidence. +`artifacts/evidence/licence-boundary.json`, retained as build evidence, without UI screenshots. Adversarial C# tests exercise declaration, inventory, import, reference and lock -failures. The existing locked restore, format, build, tests, five-RID Native AOT -UI/live-Cloud checks and immutable candidate verification remain required. +failures. Locked restore, format, offline unit tests, three Windows/Linux Native AOT builds +and one publication candidate identity/integrity check remain required. UI/live tests +are explicit local opt-in; macOS CI and repeated public-asset verification are prohibited. This change preserves published dependency versions, application identity, signing behavior and product scope. A policy pass does not prove product readiness -or close a later commercial-release gate. Current runs and post-merge assets must -be verified for their exact source commit. +or close a later commercial-release gate. Post-merge checks confirm the expected source commit and required publication result; +they do not download or execute public assets. diff --git a/docs/provenance.md b/docs/provenance.md index 7bafcd2..b300ebc 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -46,13 +46,9 @@ absent from the Native AOT application. No sibling source is consumed during bui Portable staging requires committed, audited source. It retains the root licence, six exact upstream legal documents, the generated package source summary and the -actual source-audit receipt. Packing and independent release verification read the -real ZIP/tar entries, compare full legal bytes and require a passing receipt for +actual source-audit receipt. One publication-handoff check reads the +real ZIP/tar entries, compares full legal bytes and requires a passing receipt for the candidate's clean source commit. Existing dependency notices remain intact; -the summary does not replace them. The same gates run on all five native hosts. +the summary does not replace them. The repository static-check command runs once on Linux; each of the three Windows/Linux staging steps retains its source-policy receipt and required legal material. -Locked dependencies, Native AOT, native UI actions, real Cloud requests, failure -cases and exact public release assets retain their existing gates. Source or -licence checks do not establish product completeness, OS trust signing or later -commercial readiness. Each contribution records its own observed runtime and -publication evidence after full review and successful CI. +Current CI and publication follow the [accepted validation policy](https://github.com/ArcForges/ArcForges-Design/blob/47db6670a727317939b91245e8c0b288834acf99/docs/assurance/ci-and-local-validation-policy.md). Windows/Linux compilation, offline unit/static/security checks and required provenance remain; macOS, UI/live/runtime gates and repeated public downloads are removed. These checks do not establish product completeness or commercial readiness. diff --git a/docs/release-notes.md b/docs/release-notes.md index 8fd9720..a53ebc7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,7 +1,7 @@ -Native AOT ArcNotes Hello desktop client connected to `https://arcforges.com/api`. +# ArcNotes development prerelease -Download the complete portable archive matching your OS and CPU, verify its SHA-256, and extract all files together. No .NET runtime installation or service token is needed. Windows: run `ArcNotes.exe`. Linux: run `./ArcNotes` in an X11/XWayland desktop with the documented system libraries. macOS: the archive contains `ArcNotes.app`. +Extract the complete Windows x64/ARM64 ZIP or Linux x64 tar.gz and keep its files together. No .NET runtime installation or service token is needed. Windows: run `ArcNotes.exe`. Linux: run `./ArcNotes` in an X11/XWayland desktop with the documented system libraries. -These are development prereleases. Windows binaries are unsigned; macOS bundles have ad-hoc signing only, with no Developer ID/notarization. Installer, store and automatic-update delivery are not included. +These are portable development builds. Windows binaries are unsigned; installer, store and automatic-update delivery are not included. This release has no macOS artifact. -All five native candidates passed deterministic tests and programmatic native UI/live Cloud checks before this release was published. `arcnotes-verification.tar.gz` contains source/RID/version manifests, live check results and rendered window images. The source is the commit referenced by this release tag. See the README at that tag for prerequisites and licence notices. +The candidates passed Windows/Linux compilation and the applicable offline/static/security checks. CI does not launch applications or perform UI/live Cloud tests. `arcnotes-verification.tar.gz` contains build source/RID/version manifests, not runtime proof or screenshots. The source is the commit referenced by this release tag. Checksum sidecars support optional integrity diagnostics and are not trusted code signatures. See the README at that tag for prerequisites and licence notices. diff --git a/docs/releasing.md b/docs/releasing.md index 3340ade..b183fb8 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,23 +1,17 @@ # Automatic portable releases -No release API key or publishing account is required. CI uses the repository-scoped `GITHUB_TOKEN`; only the release job receives `contents: write`. Public Hello needs no secret. Repository security features and the required `Verify` check are configured in GitHub settings, not injected into a client binary. +CI follows the [accepted validation policy](https://github.com/ArcForges/ArcForges-Design/blob/47db6670a727317939b91245e8c0b288834acf99/docs/assurance/ci-and-local-validation-policy.md). No macOS or application runtime test runs in any workflow. -1. A PR runs deterministic tests, formatting/locked restore, secret/dependency review, C#/Actions CodeQL and five native builds: Windows x64/ARM64, Linux x64, macOS x64/ARM64. -2. Each native job publishes the self-contained AOT directory, includes notices, prepares its portable layout, and runs the exact staged executable's native UI/live smoke. macOS ad-hoc signing happens before testing and archiving. -3. Only after that success does it create a candidate archive, SHA-256 checksum, source/version/RID manifest and evidence. Upload artifact names include the run ID and attempt. Failed jobs retain diagnostic evidence but cannot produce a release candidate. -4. `Verify` requires all platform and security jobs and downloads all five archives to exercise the aggregate identity/hash/evidence check before merge. A successful **push to main** alone enables automatic publication. Manual/scheduled runs and PRs validate only. -5. The release job downloads candidates from that same workflow run, verifies all five identities, hashes and evidence, then uploads those exact archives without rebuilding. Version is `0.1.0-ci..`. Re-running uses a new immutable version. It creates a draft first, uploads all assets, then makes the prerelease visible; a failed upload leaves a draft rather than a complete-looking release. +1. PRs compile Windows x64/ARM64 and Linux x64 Native AOT candidates. Formatting, source/licence checks, static assembly metadata inspection and offline unit tests run once on Linux. Secret/dependency review and C#/Actions CodeQL remain required. +2. Staging includes the complete executable directory, dependency/legal notices and build-input identity receipt without launching the app. Packing creates each archive, source/version/RID manifest and SHA-256 sidecar. Smoke results and screenshots are not release inputs. +3. The required `Verify` job checks applicable job outcomes only. It does not download or rescan artifacts. PR, manual and scheduled runs never publish. +4. A successful main push promotes the same run's three candidates. The release job retrieves them once and performs one identity, archive-integrity and licence/provenance check at this publication handoff. It does not rebuild or execute them. +5. The release job creates a draft `v0.1.0-ci..`, uploads three archives, three checksum sidecars and `arcnotes-verification.tar.gz` containing build manifests, then exposes the prerelease. `Verify publication` requires the publishing job to succeed. -Download `arcnotes-VERSION-RID.zip` or `.tar.gz` together with its `.sha256`. Evidence and package inventory describe what was exercised; a checksum detects corruption but is not a trusted code signature. Do not overwrite a published archive or repoint its Git tag. Fix the source and publish a new successful main run. Consumers can roll back by extracting an older complete version into a separate directory; this Hello version has no persisted user data or migration. +The repository-scoped `GITHUB_TOKEN` grants `contents: write` only to publication. The explicit main publication condition tolerates the intentionally skipped PR-only dependency review while requiring `Verify` success. Do not overwrite published archives or repoint tags; do not rerun publication to manufacture validation evidence. -The five candidates use native GitHub hosts, not cross-compilation as a substitute for execution. A live service outage blocks publication and preserves its error evidence. First release publication itself is verified only after the PR is merged; a green PR does not demonstrate a main-branch release has already occurred. +Provider publication success and the expected commit/version close post-merge verification. Do not routinely download public assets, compare hashes/members, install them or rerun consumers. No live service availability is a release gate. A green build is not a native UI/live test result. -Publication uses an explicit cancellation/status condition and requires `Verify` -to succeed. The PR-only dependency-review job is intentionally skipped on main; -GitHub's default ancestor-success condition must not suppress publication after -the aggregate gate has accepted that applicable job set. A final main-only -`Verify publication` job fails when publishing fails or is unexpectedly skipped. -This corrects the skipped publication observed during WP00.02 post-merge checks. -See the [GitHub job-dependency rules](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idneeds). +Current automated releases contain Windows and Linux portable development distributions only. Local macOS source/build support is retained; this workflow produces no macOS artifact or runtime claim. Windows binaries are unsigned. Installers, app stores, trusted OS signing and updates are separate delivery features. -The current distributions are portable development builds. Authenticode, Developer ID/notarization, installers, app-store delivery and update signing require separate credentials and implementation before trusted public product distribution. +A `.sha256` detects corruption; it is not a trusted code signature. Source is the release's exact tag/commit. Existing complete releases remain available as historical artifacts. diff --git a/eng/ArcForges.Repository/Program.cs b/eng/ArcForges.Repository/Program.cs index ddcd953..1d775cb 100644 --- a/eng/ArcForges.Repository/Program.cs +++ b/eng/ArcForges.Repository/Program.cs @@ -13,6 +13,7 @@ namespace ArcForges.Repository; public static partial class Program { public static readonly string[] Rids = ["win-x64", "win-arm64", "linux-x64", "osx-x64", "osx-arm64"]; + public static readonly string[] ReleaseRids = ["win-x64", "win-arm64", "linux-x64"]; private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web) { WriteIndented = true }; public static async Task Main(string[] args) @@ -36,6 +37,8 @@ public static async Task Main(string[] args) break; case ["prepare", var rid, var version]: await Prepare(rid, version); break; case ["smoke", var rid]: + if (Environment.GetEnvironmentVariable("CI") == "true" || Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true") + throw new InvalidOperationException("Runtime smoke is an explicit local-only operation."); ValidateRid(rid); Directory.CreateDirectory("artifacts/evidence"); await Run(Executable(rid), ["--smoke-live", "--evidence", Path.GetFullPath($"artifacts/evidence/{rid}.json")]); @@ -43,14 +46,15 @@ public static async Task Main(string[] args) case ["pack", var rid, var version, var commit]: await Pack(rid, version, commit); break; case ["verify", var path, var version, var commit]: var manifests = Directory.GetFiles(path, "manifest.json", SearchOption.AllDirectories); - if (manifests.Length != Rids.Length) throw new InvalidOperationException("Expected all five native candidates."); + if (manifests.Length != ReleaseRids.Length) throw new InvalidOperationException("Expected the three Windows/Linux release candidates."); var seen = new HashSet(StringComparer.Ordinal); foreach (var manifest in manifests) { var rid = VerifyCandidate(manifest, version, commit); if (!seen.Add(rid)) throw new InvalidOperationException("Duplicate candidate RID."); } - Console.WriteLine("Verified all five immutable native candidates."); + if (!seen.SetEquals(ReleaseRids)) throw new InvalidOperationException("Release candidate RID set differs from Windows/Linux producers."); + Console.WriteLine("Verified the three Windows/Linux release candidates."); break; default: throw new ArgumentException("Use hooks, check, provenance-notice, version, prepare RID VERSION, smoke RID, pack RID VERSION COMMIT, or verify DIRECTORY VERSION COMMIT."); } @@ -108,9 +112,10 @@ private static async Task Prepare(string rid, string version) ProvenancePolicy.VerifyPackageNotices(requiredNotices, path => File.Exists(Path.Combine(stage, path)) ? File.ReadAllBytes(Path.Combine(stage, path)) : null); File.Copy("artifacts/evidence/provenance.json", Path.Combine(stage, "notices/provenance-source.json")); if (!File.Exists(Executable(rid))) throw new InvalidOperationException("Published executable is missing."); - await Run(Executable(rid), ["--build-info", "--evidence", Path.Combine(stage, "build-identity.json")]); - IdentityEvidence.Verify(File.ReadAllBytes(Path.Combine(stage, "build-identity.json")), Directory.GetCurrentDirectory(), version, + // This is a build-input receipt, not an assertion that the packaged application ran. + var identity = IdentityEvidence.ExpectedReport(Directory.GetCurrentDirectory(), version, (await Capture("git", ["rev-parse", "HEAD"])).Trim()); + await File.WriteAllTextAsync(Path.Combine(stage, "build-identity.json"), identity.ToJsonString(Json)); if (rid.StartsWith("osx-", StringComparison.Ordinal)) { var parts = version.Split('.'); @@ -176,12 +181,10 @@ private static async Task Pack(string rid, string version, string commit) ValidateRid(rid); ValidateVersion(version); if (commit.Length != 40 || !commit.All(char.IsAsciiHexDigit)) throw new ArgumentException("Expected a full Git commit."); - await CheckSourceForDistribution(); + if ((await Capture("git", ["status", "--porcelain"])).Length != 0) + throw new InvalidOperationException("Commit reviewed source before packing a portable candidate."); if ((await Capture("git", ["rev-parse", "HEAD"])).Trim() != commit) throw new InvalidOperationException("Pack source differs from the candidate revision."); - var evidence = $"artifacts/evidence/{rid}.json"; - using var smoke = JsonDocument.Parse(File.ReadAllText(evidence)); - ValidateSmoke(smoke.RootElement, rid, version, commit); var folder = $"artifacts/candidate/{rid}"; Directory.CreateDirectory(folder); var extension = rid.StartsWith("win-", StringComparison.Ordinal) ? ".zip" : ".tar.gz"; @@ -195,13 +198,10 @@ private static async Task Pack(string rid, string version, string commit) using var gzip = new GZipStream(file, CompressionLevel.Optimal); TarFile.CreateFromDirectory(Stage(rid), gzip, false); } - VerifyArchiveNotices(archive, commit, Directory.GetCurrentDirectory(), version); - File.Copy(evidence, Path.Combine(folder, "smoke.json")); - File.Copy(Path.ChangeExtension(evidence, ".png"), Path.Combine(folder, "screen.png")); - var manifest = new Candidate(rid, version, commit, name, Hash(archive), Hash(evidence)); + var manifest = new Candidate(rid, version, commit, name, Hash(archive)); File.WriteAllText(Path.Combine(folder, "manifest.json"), JsonSerializer.Serialize(manifest, Json)); File.WriteAllText(archive + ".sha256", $"{manifest.ArchiveSha256} {name}\n"); - Console.WriteLine($"Packed verified {rid}: {name}"); + Console.WriteLine($"Packed compiled {rid}: {name}"); } public static string VerifyCandidate(string manifestPath, string version, string commit, string? sourceRoot = null) @@ -214,13 +214,10 @@ public static string VerifyCandidate(string manifestPath, string version, string if (manifest.Archive != expected) throw new InvalidOperationException("Invalid archive path."); var folder = Path.GetDirectoryName(manifestPath)!; var archive = Path.Combine(folder, manifest.Archive); - var smoke = Path.Combine(folder, "smoke.json"); - if (Hash(archive) != manifest.ArchiveSha256 || Hash(smoke) != manifest.SmokeSha256) + if (Hash(archive) != manifest.ArchiveSha256) throw new InvalidOperationException("Candidate hash mismatch."); if (File.ReadAllText(archive + ".sha256") != $"{manifest.ArchiveSha256} {manifest.Archive}\n") throw new InvalidOperationException("Download checksum mismatch."); - using var evidence = JsonDocument.Parse(File.ReadAllText(smoke)); - ValidateSmoke(evidence.RootElement, manifest.Rid, version, commit); VerifyArchiveNotices(archive, commit, sourceRoot ?? Directory.GetCurrentDirectory(), version); return manifest.Rid; } @@ -278,17 +275,6 @@ void Retain(string path, Stream? stream) throw new InvalidOperationException("Candidate provenance does not match clean reviewed source."); } - private static void ValidateSmoke(JsonElement smoke, string rid, string version, string commit) - { - if (!smoke.GetProperty("success").GetBoolean() || !smoke.GetProperty("nativeAot").GetBoolean() || - smoke.GetProperty("rid").GetString() != rid || smoke.GetProperty("sourceRevision").GetString() != commit || - smoke.GetProperty("version").GetString() != version || smoke.GetProperty("uiGreeting").GetString() != "Hello, ArcNotes!" || - !smoke.GetProperty("cloud").GetProperty("nativeAot").GetBoolean()) - throw new InvalidOperationException("Native UI/live evidence does not match the candidate."); - var expected = new[] { "native-ui-live-action", "unicode-whitespace-boundary", "InvalidArgument", "ResourceExhausted" }; - if (!smoke.GetProperty("checks").EnumerateArray().Select(c => c.GetString()).SequenceEqual(expected)) - throw new InvalidOperationException("Incomplete native UI/live checks."); - } private static string Hash(string path) { using var stream = File.OpenRead(path); @@ -376,4 +362,4 @@ private static async Task Capture(string command, string[] args) private static async Task Run(string command, string[] args) => Console.Write(await Capture(command, args)); } -public sealed record Candidate(string Rid, string Version, string SourceRevision, string Archive, string ArchiveSha256, string SmokeSha256); +public sealed record Candidate(string Rid, string Version, string SourceRevision, string Archive, string ArchiveSha256); diff --git a/tests/ArcForges.ArcNotes.Tests/CandidateTests.cs b/tests/ArcForges.ArcNotes.Tests/CandidateTests.cs index fd3df14..8e18d81 100644 --- a/tests/ArcForges.ArcNotes.Tests/CandidateTests.cs +++ b/tests/ArcForges.ArcNotes.Tests/CandidateTests.cs @@ -29,12 +29,10 @@ public void ReleaseRejectsTamperingOrWrongSource() Assert.Throws(() => Program.VerifyCandidate(fixture.Manifest, fixture.Version, fixture.Commit, fixture.SourceRoot)); } - [Theory] - [InlineData("../outside.zip", true)] - [InlineData(null, false)] - public void ReleaseRejectsPathEscapeOrFailedLiveEvidence(string? unsafeArchive, bool success) + [Fact] + public void ReleaseRejectsPathEscape() { - using var fixture = new CandidateFixture(unsafeArchive, success); + using var fixture = new CandidateFixture("../outside.zip"); Assert.Throws(() => Program.VerifyCandidate(fixture.Manifest, fixture.Version, fixture.Commit, fixture.SourceRoot)); } @@ -81,7 +79,7 @@ private sealed class CandidateFixture : IDisposable public string Manifest => Path.Combine(_folder, "manifest.json"); public string Archive { get; } - public CandidateFixture(string? unsafeArchive = null, bool success = true, string rid = "win-x64", string mode = "valid") + public CandidateFixture(string? unsafeArchive = null, string rid = "win-x64", string mode = "valid") { var repo = new DirectoryInfo(AppContext.BaseDirectory); while (!File.Exists(Path.Combine(repo.FullName, ProvenancePolicy.Policy))) repo = repo.Parent ?? throw new InvalidOperationException("Repository policy not found."); @@ -123,22 +121,10 @@ public CandidateFixture(string? unsafeArchive = null, bool success = true, strin tar.WriteEntry(new PaxTarEntry(TarEntryType.RegularFile, notice.Key) { DataStream = data }); } } - var smoke = Path.Combine(_folder, "smoke.json"); - File.WriteAllText(smoke, JsonSerializer.Serialize(new - { - success, - nativeAot = true, - rid, - sourceRevision = Commit, - version = Version, - uiGreeting = "Hello, ArcNotes!", - cloud = new { nativeAot = true }, - checks = new[] { "native-ui-live-action", "unicode-whitespace-boundary", "InvalidArgument", "ResourceExhausted" } - })); var hash = Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(Archive))); File.WriteAllText(Archive + ".sha256", $"{hash} {Path.GetFileName(Archive)}\n"); File.WriteAllText(Manifest, JsonSerializer.Serialize(new Candidate(rid, Version, Commit, - unsafeArchive ?? Path.GetFileName(Archive), hash, Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(smoke)))))); + unsafeArchive ?? Path.GetFileName(Archive), hash))); } public void Dispose() => Directory.Delete(_folder, true);