From b7e9e8cfec3fe71100738401eeae5f488e422ce6 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Sun, 27 Sep 2026 03:33:06 +0000 Subject: [PATCH 1/4] test(app): name a missing secret finding without printing the others The code-scanning fix removed the found findings from this test's failure message, but left a closure that rustfmt formats differently, so the formatting check failed on main. The message now names the expected rule and file, which carry no secret material. --- crates/repodna-app/tests/fixtures.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/repodna-app/tests/fixtures.rs b/crates/repodna-app/tests/fixtures.rs index af36008..6a25497 100644 --- a/crates/repodna-app/tests/fixtures.rs +++ b/crates/repodna-app/tests/fixtures.rs @@ -229,9 +229,8 @@ fn suspicious_secrets() { secrets .iter() .find(|s| s.rule == rule && s.path == path) - .unwrap_or_else(|| { - panic!("missing expected secret finding") - }) + // Names the expected rule and file, never the findings themselves. + .unwrap_or_else(|| panic!("no {rule} finding in {path}")) }; assert!(!found("aws-access-key-id", "deploy/config.py").in_test_or_example); assert!(found("aws-access-key-id", "tests/fixtures/credentials.json").in_test_or_example); From 9988c4904cdfbb7de9511b27fc8b1d78c08644b2 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Sun, 27 Sep 2026 03:38:15 +0000 Subject: [PATCH 2/4] feat(release): trial builds, and a container image on the GitHub Container Registry Running the Release workflow by hand without a tag now builds every file from the selected branch as a trial and keeps them as downloads of the run, without publishing anything, so a release can be checked before it is tagged. Every job builds the same ref: the full tag reference, or the trial's exact commit. Each release also pushes ghcr.io/sanskarin/repodna for linux/amd64 and linux/arm64, tagged with the version, the minor and major versions, and latest. The image is Alpine Linux with Git and the release's static repodna binary; it trusts mounted repositories so that history analysis works on checkouts owned by another user, keeps its storage in /tmp/repodna so it also runs with --user, and carries the license files. The workflow tests the amd64 image on the checkout before it pushes. --- .github/workflows/release.yml | 130 ++++++++++++++++++++++++++++----- CHANGELOG.md | 4 +- README.md | 4 + docs/development.md | 8 +- docs/installation.md | 22 ++++++ packaging/container/Dockerfile | 40 ++++++++++ 6 files changed, 189 insertions(+), 19 deletions(-) create mode 100644 packaging/container/Dockerfile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d32683..7d85250 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,19 @@ name: Release -# Builds and publishes a release when a version tag is pushed. Running it by hand for an -# existing tag rebuilds that tag's code with this workflow and updates the release's files. +# Builds and publishes a release when a version tag is pushed: the command line for every +# platform, the desktop installers, and the container image on ghcr.io. Running it by hand +# with an existing tag rebuilds that tag's code and updates the release's files. Running it +# by hand without a tag is a trial: it builds every file from the selected branch and keeps +# them as downloads of the run, without publishing anything. on: push: tags: ["v*"] workflow_dispatch: inputs: tag: - description: Existing tag to build and publish, for example v1.0.0 - required: true + description: Existing tag to build and publish, such as v1.0.0. Leave it empty for a trial build of the selected branch. + required: false + default: "" permissions: contents: read @@ -24,28 +28,45 @@ jobs: outputs: tag: ${{ steps.version.outputs.tag }} version: ${{ steps.version.outputs.version }} + # What every job builds: the full tag ref (never a branch of the same name) or, for a + # trial, the exact commit. + ref: ${{ steps.version.outputs.ref }} + publish: ${{ steps.version.outputs.publish }} steps: - # Full tag refs everywhere: a branch with the same name as the tag must not be built. - uses: actions/checkout@v7 with: ref: ${{ inputs.tag && format('refs/tags/{0}', inputs.tag) || github.ref }} - id: version env: - TAG: ${{ inputs.tag || github.ref_name }} + # Empty for a trial started by hand without a tag. + TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }} run: | set -euo pipefail - version="${TAG#v}" workspace=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "repodna-cli") | .version') - if [ "$version" != "$workspace" ]; then - echo "The tag $TAG does not match the workspace version $workspace." - exit 1 + if [ -n "$TAG" ]; then + version="${TAG#v}" + if [ "$version" != "$workspace" ]; then + echo "The tag $TAG does not match the workspace version $workspace." + exit 1 + fi + ref="refs/tags/$TAG" + publish=true + else + version="$workspace" + ref=$(git rev-parse HEAD) + publish=false + echo "Trial build of $version from $GITHUB_REF_NAME at $ref; nothing is published." fi if ! grep -q "^## \[$version\]" CHANGELOG.md; then echo "CHANGELOG.md has no section for $version." exit 1 fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "version=$version" >> "$GITHUB_OUTPUT" + { + echo "tag=$TAG" + echo "version=$version" + echo "ref=$ref" + echo "publish=$publish" + } >> "$GITHUB_OUTPUT" web: name: Web interface @@ -54,7 +75,7 @@ jobs: steps: - uses: actions/checkout@v7 with: - ref: refs/tags/${{ needs.prepare.outputs.tag }} + ref: ${{ needs.prepare.outputs.ref }} - uses: actions/setup-node@v7 with: node-version: 22 @@ -88,7 +109,7 @@ jobs: steps: - uses: actions/checkout@v7 with: - ref: refs/tags/${{ needs.prepare.outputs.tag }} + ref: ${{ needs.prepare.outputs.ref }} # The binary embeds the web interface served by `repodna serve`. - uses: actions/download-artifact@v8 with: @@ -161,7 +182,7 @@ jobs: steps: - uses: actions/checkout@v7 with: - ref: refs/tags/${{ needs.prepare.outputs.tag }} + ref: ${{ needs.prepare.outputs.ref }} - name: Install WebKitGTK if: runner.os == 'Linux' run: | @@ -194,16 +215,89 @@ jobs: path: dist/* if-no-files-found: error + container: + name: Container image + needs: [prepare, cli] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.prepare.outputs.ref }} + - uses: actions/download-artifact@v8 + with: + pattern: cli-*-linux-musl + path: archives + merge-multiple: true + - name: Prepare the build context + # The static Linux binaries of this release, and the license files. + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + context="$RUNNER_TEMP/image" + for pair in amd64:x86_64 arm64:aarch64; do + arch="${pair%%:*}" + name="repodna-$VERSION-${pair#*:}-unknown-linux-musl" + tar -xzf "archives/$name.tar.gz" -C "$RUNNER_TEMP" + mkdir -p "$context/$arch" + cp "$RUNNER_TEMP/$name/repodna" "$context/$arch/repodna" + done + cp LICENSE NOTICE THIRD-PARTY-NOTICES.txt "$context/" + echo "CONTEXT=$context" >> "$GITHUB_ENV" + - name: Set up emulation and Buildx + # Installing Git into the arm64 image runs under emulation on this x86_64 runner. + run: | + docker run --privileged --rm tonistiigi/binfmt --install arm64 + docker buildx create --use --name repodna + - name: Test the image + run: | + set -euo pipefail + docker buildx build --platform linux/amd64 --load --tag repodna:test \ + --file packaging/container/Dockerfile "$CONTEXT" + docker run --rm repodna:test --version + docker run --rm --volume "$PWD:/work" repodna:test analyze . --profile quick --no-store --quiet > /dev/null + - name: Log in to the GitHub Container Registry + if: needs.prepare.outputs.publish == 'true' + env: + TOKEN: ${{ github.token }} + run: echo "$TOKEN" | docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin + - name: Build for amd64 and arm64, and push a release + env: + VERSION: ${{ needs.prepare.outputs.version }} + PUBLISH: ${{ needs.prepare.outputs.publish }} + run: | + set -euo pipefail + image="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/repodna" + tags=(--tag "$image:$VERSION") + if [[ "$VERSION" != *-* ]]; then + major="${VERSION%%.*}" + minor="${VERSION#*.}" + minor="${minor%%.*}" + tags+=(--tag "$image:$major.$minor" --tag "$image:$major" --tag "$image:latest") + fi + push=() + if [ "$PUBLISH" = true ]; then + push=(--push) + fi + docker buildx build --platform linux/amd64,linux/arm64 \ + --build-arg VERSION="$VERSION" --build-arg REVISION="$(git rev-parse HEAD)" \ + --label org.opencontainers.image.created="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + "${tags[@]}" "${push[@]}" --file packaging/container/Dockerfile "$CONTEXT" + publish: name: Publish the release - needs: [prepare, cli, desktop] + needs: [prepare, cli, desktop, container] + if: needs.prepare.outputs.publish == 'true' runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v7 with: - ref: refs/tags/${{ needs.prepare.outputs.tag }} + ref: ${{ needs.prepare.outputs.ref }} - uses: actions/download-artifact@v8 with: pattern: cli-* @@ -239,6 +333,8 @@ jobs: - `RepoDNA__amd64.deb`, `RepoDNA--1.x86_64.rpm`, `RepoDNA__universal.dmg`, `RepoDNA__x64_en-US.msi`, and `RepoDNA__x64-setup.exe`: the desktop app. + - `ghcr.io/sanskarin/repodna:`: a container image with the command line and + Git, for CI jobs: `docker run --rm -v "$PWD:/work" ghcr.io/sanskarin/repodna analyze .` - `SHA256SUMS.txt`: checksums of every file. EOF diff --git a/CHANGELOG.md b/CHANGELOG.md index 286510e..b47d389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to RepoDNA are documented in this file. The format is based ## [Unreleased] -## [1.0.0] - 2026-09-26 +## [1.0.0] - 2026-09-27 The first stable release: local-first repository intelligence and code archaeology, with every conclusion backed by evidence. @@ -71,6 +71,8 @@ every conclusion backed by evidence. part of the analysis, with search, a command palette, keyboard shortcuts, light and dark themes, a table view for every chart, and a bundled demo that works offline. - A desktop app for Linux, macOS, and Windows, built with Tauri on the same Rust core. +- A container image with the command line and Git, `ghcr.io/sanskarin/repodna`, for + `linux/amd64` and `linux/arm64`. - About & support, Privacy Policy, Terms of Use, and Licenses pages in the web interface and the desktop app; every download includes the licenses of the third-party software it contains (`THIRD-PARTY-NOTICES.txt`). diff --git a/README.md b/README.md index d5fbf8f..9eab58d 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,10 @@ repodna --version **The desktop app.** Installers for Linux (`.deb`, `.rpm`), macOS (`.dmg`), and Windows (`.msi`, `.exe`) are attached to each release. See [the desktop app](docs/desktop.md). +**Container image.** `docker run --rm -v "$PWD:/work" ghcr.io/sanskarin/repodna analyze .` +runs the command line with Git, on `linux/amd64` and `linux/arm64`. See +[the container image](docs/installation.md#container-image). + **The web version.** needs no installation: it opens analyses (`repodna.json` or `.repodna` files) in your browser, without uploading them, and includes the demo. Analyzing a repository needs the command line or the desktop app. diff --git a/docs/development.md b/docs/development.md index d7f6480..4066e96 100644 --- a/docs/development.md +++ b/docs/development.md @@ -230,4 +230,10 @@ cargo xtask bench --runs 5 # the table in benchmarks/README.md The [release workflow](../.github/workflows/release.yml) checks that the tag matches the workspace version and the changelog, builds the command line for Linux, macOS, and Windows -and the desktop installers, and publishes them with checksums. +and the desktop installers, publishes them with checksums, and pushes the container image +to `ghcr.io/sanskarin/repodna`. After the first release, make the image public once in the +package's settings on GitHub (**Package settings > Change visibility**). + +To try a release before tagging it, run the Release workflow by hand from the Actions tab +with an empty tag: it builds every file from the selected branch and keeps them as +downloads of the run, without publishing anything. diff --git a/docs/installation.md b/docs/installation.md index c38b692..2553f64 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,6 +6,7 @@ installing anything, use the [web version](web.md#the-web-version). - [Prebuilt binaries](#prebuilt-binaries) - [The desktop app](#the-desktop-app) +- [Container image](#container-image) - [Build from source](#build-from-source) - [Check the installation](#check-the-installation) - [Uninstall](#uninstall) @@ -70,6 +71,27 @@ The binaries and installers are not code-signed. Installers for Linux (`.deb`, `.rpm`), macOS (`.dmg`), and Windows (`.msi`, `.exe`) are attached to each release. See [the desktop app](desktop.md). +## Container image + +Each release is also published as a container image with the command line and Git, for CI +jobs and machines where you would rather not install anything. It runs on `linux/amd64` and +`linux/arm64`: + +```sh +docker run --rm -v "$PWD:/work" ghcr.io/sanskarin/repodna analyze . +docker run --rm -v "$PWD:/work" --user "$(id -u):$(id -g)" \ + ghcr.io/sanskarin/repodna report . --output repodna-report +``` + +The current directory is mounted as `/work`, and `--user` makes the files RepoDNA writes +yours. Tags follow the releases: `1.0.0`, `1.0`, `1`, and `latest`. Stored analyses live in +`/tmp/repodna` inside the container and disappear with it; mount a volume there +(`-v repodna-data:/tmp/repodna`) to keep them. `repodna serve` in a container listens on the +container's own loopback address, so use an installed binary or the desktop app for the web +interface. The image is based on Alpine Linux; the Alpine packages in it, such as Git, keep +their own licenses, and their sources are available from +[Alpine Linux](https://gitlab.alpinelinux.org/alpine/aports). + ## Build from source Prerequisites: diff --git a/packaging/container/Dockerfile b/packaging/container/Dockerfile new file mode 100644 index 0000000..210d233 --- /dev/null +++ b/packaging/container/Dockerfile @@ -0,0 +1,40 @@ +# syntax=docker/dockerfile:1 +# +# The repodna command line with Git, for CI jobs and machines without an installation. +# The release workflow builds it for linux/amd64 and linux/arm64 from the static Linux +# binaries it publishes, and pushes it to ghcr.io/sanskarin/repodna. The build context holds: +# +# amd64/repodna, arm64/repodna the binaries +# LICENSE, NOTICE, THIRD-PARTY-NOTICES.txt +# +# Usage: docker run --rm -v "$PWD:/work" ghcr.io/sanskarin/repodna analyze . + +FROM alpine:3.24 + +ARG TARGETARCH +ARG VERSION=dev +ARG REVISION=unknown + +LABEL org.opencontainers.image.title="RepoDNA" \ + org.opencontainers.image.description="Local-first repository intelligence and code archaeology: the repodna command line with Git." \ + org.opencontainers.image.source="https://github.com/sanskarIN/RepoDNA" \ + org.opencontainers.image.url="https://github.com/sanskarIN/RepoDNA" \ + org.opencontainers.image.documentation="https://github.com/sanskarIN/RepoDNA/blob/main/docs/installation.md#container-image" \ + org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.vendor="Sanskar" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.revision="${REVISION}" + +# Git for history analysis. Mounted repositories usually belong to another user, which Git +# refuses unless it is told to trust them. +RUN apk add --no-cache git ca-certificates \ + && git config --system --add safe.directory '*' + +COPY --chmod=0755 ${TARGETARCH}/repodna /usr/local/bin/repodna +COPY LICENSE NOTICE THIRD-PARTY-NOTICES.txt /usr/share/doc/repodna/ + +# Storage in a directory any user can write, so the image also runs with --user. +ENV REPODNA_HOME=/tmp/repodna +WORKDIR /work +ENTRYPOINT ["repodna"] +CMD ["--help"] From db18cb508ee6d8f2f8ce564567e6a6753f71eae4 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Sun, 27 Sep 2026 03:39:14 +0000 Subject: [PATCH 3/4] fix(release): leave link definitions out of the notes and name the version The release notes copied the changelog's link definitions, which follow the last section, and listed the downloads with a placeholder. They now end with the section's own text, and the download names carry the version. --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d85250..3945e93 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -319,9 +319,11 @@ jobs: VERSION: ${{ needs.prepare.outputs.version }} run: | set -euo pipefail + # The version's section of the changelog, without the link definitions at its end. awk -v version="$VERSION" ' index($0, "## [" version "]") == 1 { found = 1; next } found && /^## \[/ { exit } + found && /^\[[^]]+\]: / { next } found { print } ' CHANGELOG.md > notes.md cat >> notes.md <<'EOF' @@ -339,6 +341,7 @@ jobs: EOF echo "The binaries are not code-signed, so macOS and Windows ask for confirmation the first time they start; see the [installation guide](https://github.com/sanskarIN/RepoDNA/blob/$TAG/docs/installation.md)." >> notes.md + sed -i "s//$VERSION/g" notes.md cat notes.md - name: Create or update the release env: From bc1c52b863becd9328a0237dd8dbeeacb4dbb2d6 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Sun, 27 Sep 2026 04:00:03 +0000 Subject: [PATCH 4/4] fix(desktop): sign the macOS app ad hoc so a downloaded copy opens Without a signature that covers the whole bundle, Macs with Apple silicon report the downloaded app as damaged and offer no way to open it. The bundle is now signed ad hoc, so macOS asks for confirmation in Privacy & Security instead, and the release workflow checks the signature before it collects the installers. The installation guide and the desktop page now describe Open Anyway in System Settings, which replaced Control-click Open in macOS 15. --- .github/workflows/release.yml | 8 ++++++++ apps/desktop/README.md | 4 ++++ apps/desktop/src-tauri/tauri.conf.json | 5 ++++- docs/desktop.md | 7 ++++--- docs/installation.md | 13 ++++++++----- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3945e93..03eef91 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -199,6 +199,14 @@ jobs: # Builds the web interface first (Tauri's beforeBuildCommand) and embeds it. - name: Build the installers run: npm run bundle -w @repodna/desktop -- ${{ matrix.bundle-args }} + - name: Check the app signature + # The ad-hoc signature must cover the whole bundle: macOS reports a downloaded app + # without one as damaged, instead of offering to open it from Privacy & Security. + if: runner.os == 'macOS' + run: | + app=apps/desktop/src-tauri/target/universal-apple-darwin/release/bundle/macos/RepoDNA.app + codesign --verify --deep --strict --verbose=2 "$app" + codesign --display --verbose=2 "$app" - name: Collect the installers shell: bash run: | diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 52a0fa3..52102e1 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -35,6 +35,10 @@ This builds the web interface, embeds it, and writes installers for your platfor `.dmg` on macOS, and `.msi` and `.exe` installers on Windows. Choose formats with `npm run bundle -w @repodna/desktop -- --bundles deb`. +On macOS the app is signed ad hoc (`bundle.macOS.signingIdentity` is `-` in +[tauri.conf.json](src-tauri/tauri.conf.json)): Macs with Apple silicon report a downloaded +app whose signature does not cover the whole bundle as damaged, and refuse to open it. + Without the Tauri command line, `cargo build --release --features custom-protocol` in `src-tauri/` builds the app binary with the interface embedded (build the web interface first with `npm run build -w @repodna/web` from the repository root). diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index e00e0a5..4945f1a 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -40,6 +40,9 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "macOS": { + "signingIdentity": "-" + } } } diff --git a/docs/desktop.md b/docs/desktop.md index 4a5bb92..fea684e 100644 --- a/docs/desktop.md +++ b/docs/desktop.md @@ -20,9 +20,10 @@ Installers are attached to each [release](https://github.com/sanskarIN/RepoDNA/r On Linux the app needs WebKitGTK 4.1 (`libwebkit2gtk-4.1-0`), which the packages declare as a dependency. The Linux packages are built on Ubuntu 24.04. -The installers are not code-signed. macOS asks for confirmation the first time: open the -app from Finder with Control-click, **Open**. Windows SmartScreen may show "Windows -protected your PC": choose **More info**, then **Run anyway**. See +The installers are not signed with a developer certificate, so the system asks for +confirmation the first time. On macOS, open the app once, then choose **Open Anyway** in +**System Settings > Privacy & Security**. Windows SmartScreen may show "Windows protected +your PC": choose **More info**, then **Run anyway**. See [installation](installation.md#unsigned-binaries). To build it yourself, see [apps/desktop/README.md](../apps/desktop/README.md). diff --git a/docs/installation.md b/docs/installation.md index 2553f64..0ceda17 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -58,11 +58,14 @@ Get-FileHash .\repodna-1.0.0-x86_64-pc-windows-msvc.zip -Algorithm SHA256 # co ### Unsigned binaries -The binaries and installers are not code-signed. - -- **macOS** blocks unsigned programs downloaded from the internet. For the command line, - remove the quarantine attribute: `xattr -d com.apple.quarantine /usr/local/bin/repodna`. - For the desktop app, Control-click it in Finder, choose **Open**, and confirm. +The binaries and installers are not signed with a developer certificate. + +- **macOS** blocks programs downloaded from the internet that Apple has not checked. For the + command line, remove the quarantine attribute: + `xattr -d com.apple.quarantine /usr/local/bin/repodna`. For the desktop app, open it once + and close the warning, then open **System Settings > Privacy & Security**, choose + **Open Anyway** next to the message about RepoDNA, and confirm. On macOS 14 and earlier, + you can instead Control-click the app in Finder and choose **Open**. - **Windows** SmartScreen may say "Windows protected your PC". Choose **More info**, then **Run anyway**.