diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9d41dec..4af10f2 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -160,4 +160,5 @@ jobs: test -f .streamlit/config.toml test -f .github/dependabot.yml test -f docs/L6_DEPLOYMENT_HYGIENE.md + test -f docs/RELEASING.md echo "HelixAgent release-readiness contract passed." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91fdc2c..b258789 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,33 +4,216 @@ on: push: tags: - "v*.*.*" - workflow_dispatch: + pull_request: + paths: + - ".github/workflows/release.yml" + - "CHANGELOG.md" + - "docs/RELEASING.md" permissions: - contents: write - packages: write + contents: read + +concurrency: + group: helixagent-release-${{ github.ref }} + cancel-in-progress: false jobs: + validate: + name: Validate release candidate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + - name: Verify tag, package version, and changelog entry + if: startsWith(github.ref, 'refs/tags/') + run: | + tag_name="${{ github.ref_name }}" + version="${{ github.ref_name }}" + version="${version#v}" + test "${{ github.event.created }}" = "true" + git cat-file -e "${tag_name}^{tag}" + if [[ ! "$tag_name" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Ref is not a semantic-version tag: $tag_name" + exit 1 + fi + grep -Fq "version = \"$version\"" pyproject.toml + awk -v version="$version" ' + $0 ~ "^## \\[" version "\\]" { found = 1 } + END { exit !found } + ' CHANGELOG.md + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + cache: pip + - name: Install declared development dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + - name: Run Ruff + run: ruff check api agent src tests streamlit_app.py --select E9,F63,F7,F82 + - name: Run tests + env: + PYTHONPATH: . + run: pytest + - name: Build container and verify optional C++ interop + run: | + docker build --pull -t helixagent-release:${{ github.sha }} . + docker run --rm helixagent-release:${{ github.sha }} python - <<'PY' + from agent.agent_core import ( + cosine_similarity_cpp, + cpp_backend_available, + ) + + assert cpp_backend_available() + assert cosine_similarity_cpp([1.0, 0.0], [1.0, 0.0]) == 1.0 + print("Optional C++ ctypes interop verified in the release image.") + PY + + security: + name: Security and SBOM + needs: validate + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + - uses: github/codeql-action/init@v4 + with: + languages: python + - uses: github/codeql-action/autobuild@v4 + - uses: github/codeql-action/analyze@v4 + - name: Scan current source tree for secrets + run: | + docker run --rm \ + -v "$PWD:/repo" \ + -w /repo \ + ghcr.io/gitleaks/gitleaks:latest \ + detect --source . --no-git --redact --no-banner --exit-code 1 + - uses: anchore/sbom-action@v0 + with: + path: . + format: cyclonedx-json + output-file: sbom.cdx.json + - uses: actions/upload-artifact@v4 + with: + name: helixagent-release-sbom + path: sbom.cdx.json + if-no-files-found: error + release: - name: Publish Release + name: Create immutable evidence release + if: startsWith(github.ref, 'refs/tags/') + needs: [validate, security] runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v7 - - name: Build source archive + with: + ref: ${{ github.sha }} + fetch-depth: 0 + - uses: actions/download-artifact@v4 + with: + name: helixagent-release-sbom + path: dist + - name: Refuse to overwrite an existing release + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view "${{ github.ref_name }}" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Release ${{ github.ref_name }} already exists; refusing to overwrite it." + exit 1 + fi + - name: Build source archive and checksum run: | mkdir -p dist - tar --exclude=.git --exclude=dist -czf dist/helixagent-${GITHUB_REF_NAME}.tar.gz . + git archive --format=tar --prefix="helixagent-${{ github.ref_name }}/" "${{ github.sha }}" | gzip -n > "dist/helixagent-${{ github.ref_name }}.tar.gz" + sha256sum "dist/helixagent-${{ github.ref_name }}.tar.gz" > "dist/helixagent-${{ github.ref_name }}.tar.gz.sha256" + - name: Prepare evidence-bound release notes + run: | + version="${{ github.ref_name }}" + version="${version#v}" + awk -v version="$version" ' + $0 ~ "^## \\[" version "\\]" { in_section = 1; next } + in_section && /^## / { exit } + in_section { print } + ' CHANGELOG.md > dist/changelog-section.md + test -s dist/changelog-section.md + cat > dist/reproduction.md <<'EOF' + ## Reproduction + + The tagged commit was validated by the release workflow with: + + ~~~bash + pip install -r requirements-dev.txt + ruff check api agent src tests streamlit_app.py --select E9,F63,F7,F82 + pytest + docker build -t helixagent-release . + ~~~ + + Vector backend measurements are intentionally not generated on shared GitHub-hosted + runners. Run them on the target host and retain the JSON artifact with its environment + metadata: + + ~~~bash + python -m benchmarks.vector_ops --output vector-ops-results.json + ~~~ + EOF + { + echo "# HelixAgent ${{ github.ref_name }}" + echo + echo "## What changed" + cat dist/changelog-section.md + echo + echo "## Validation" + echo "- Ruff correctness gate: passed" + echo "- Tests: passed" + echo "- Release-image build and optional C++ ctypes interop: passed" + echo "- CodeQL and secret scan: passed" + echo "- CycloneDX SBOM: generated and attached" + echo + echo "## Vector backend architecture" + echo "NumPy/BLAS default" + echo " |" + echo " +--> optional C++ ctypes backend (interop demonstration)" + echo " |" + echo " +--> pure-Python fallback" + echo + echo "The C++ backend demonstrates safe native interoperability and is not claimed to outperform NumPy/BLAS." + echo + cat dist/reproduction.md + } > dist/release-notes.md - name: Publish GitHub Release uses: softprops/action-gh-release@v2 with: - files: dist/*.tar.gz - generate_release_notes: true + tag_name: ${{ github.ref_name }} + target_commitish: ${{ github.sha }} + body_path: dist/release-notes.md + generate_release_notes: false + files: | + dist/helixagent-${{ github.ref_name }}.tar.gz + dist/helixagent-${{ github.ref_name }}.tar.gz.sha256 + dist/sbom.cdx.json + dist/reproduction.md publish-container: - name: Publish GHCR Image + name: Publish validated GHCR image + if: startsWith(github.ref, 'refs/tags/') + needs: release runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - uses: actions/checkout@v7 + with: + ref: ${{ github.sha }} - uses: docker/login-action@v4 with: registry: ghcr.io @@ -40,6 +223,10 @@ jobs: id: meta with: images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=${{ github.ref_name }} - uses: docker/build-push-action@v6 with: context: . diff --git a/CHANGELOG.md b/CHANGELOG.md index 362b758..530f0bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,16 @@ The project follows Semantic Versioning and the Keep a Changelog format. - CodeQL, Gitleaks, Trivy, pip-audit, Dependabot, and CycloneDX SBOM automation. - GitHub Release artifacts and GHCR image publishing. - Security, contribution, release-readiness, and nine-tier deployment-hygiene documentation. +- Evidence-driven semantic-tag release validation with source checksums, CycloneDX SBOM attachment, and reproducibility instructions. +- Three-way vector-backend benchmark infrastructure that writes measurements only when executed. ### Changed - Hardened `DataIngestor` with file validation, split-parameter validation, deterministic partitioning, duplicate-column detection, and explicit types. - Reworked the production image into isolated Java, C++, Python build stages and a non-root runtime stage. +- Made NumPy/BLAS the default cosine-similarity backend; the C++ ctypes backend is explicit opt-in interoperability and pure Python remains the degradation path. +- Hardened the ctypes boundary by coercing vectors to contiguous float64 buffers before pointer passing. +- Corrected vector-backend documentation to remove unsupported C++ performance claims. ## [1.0.0] - 2025-06-20 diff --git a/README.md b/README.md index a48aeaf..0fdba6c 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,19 @@ docker build -t helixagent . docker run --rm -p 8000:8000 helixagent ``` +## Releases and reproducibility + +Maintainers create releases by pushing a validated semantic-version tag; the tag workflow +validates the exact commit before it can create a GitHub Release. + +~~~bash +git tag -a vX.Y.Z -m "vX.Y.Z" +git push origin vX.Y.Z +~~~ + +See [release procedure and evidence artifacts](docs/RELEASING.md) for the required +changelog/version update, validation, and reproducibility guidance. + ## Project map ```text diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..f42cf23 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,71 @@ +# Releasing HelixAgent + +## Release policy + +HelixAgent uses semantic versions and immutable, tag-driven evidence releases. + +- **PATCH**: bug fixes, documentation corrections, and small internal improvements. +- **MINOR**: backward-compatible capabilities or meaningful engineering milestones. +- **MAJOR**: intentional breaking public API changes. + +A commit message does not determine a version. The maintainer selects it after reviewing +the public API, the changelog, and the validation evidence. + +## Preflight + +Before creating a release tag: + +1. Merge the intended change set to `main`. +2. Set `[project].version` in `pyproject.toml` to the release version without the + `v` prefix. +3. Move the corresponding items from `[Unreleased]` into a dated + `## [X.Y.Z]` section in `CHANGELOG.md`. +4. Run the repository validation commands: + + ~~~bash + pip install -r requirements-dev.txt + ruff check api agent src tests streamlit_app.py --select E9,F63,F7,F82 + pytest + docker build -t helixagent-release . + ~~~ + +5. Review the changelog for evidence-bound statements only. Do not add benchmark, + coverage, or performance values unless they came from a documented run. + +The release workflow verifies the version/tag and changelog-section consistency. A +mismatch fails before publication. + +## Create a release + +From the validated `main` commit: + +~~~bash +git tag -a vX.Y.Z -m "vX.Y.Z" +git push origin vX.Y.Z +~~~ + +Only newly-created annotated semantic-version tags matching `v*.*.*` start the +release workflow. The workflow rejects tag-update events, checks out the exact tag +commit, runs validation and security/SBOM work, then creates a GitHub Release only +if those required jobs succeed. It refuses to overwrite an existing GitHub Release. + +## Evidence artifacts + +A successful release attaches: + +- a source archive made from the tagged commit; +- a SHA-256 checksum for that archive; +- a CycloneDX SBOM; and +- release reproduction instructions. + +The workflow does not run vector timing on GitHub-hosted hardware because shared +runner measurements are not a useful performance claim. Reproduce the vector +experiment on the target host instead: + +~~~bash +python -m benchmarks.vector_ops --output vector-ops-results.json +~~~ + +NumPy/BLAS is the default vector backend. The C++ ctypes backend demonstrates safe +native interoperability and is not claimed to outperform NumPy/BLAS. Pure Python +is the fallback when NumPy is unavailable.