Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 124 additions & 17 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,28 +28,45 @@
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
Expand All @@ -54,14 +75,14 @@
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
cache: npm
- run: npm ci
- run: npm run build -w @repodna/web

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.prepare.outputs.ref
. (
workflow_dispatch
).
- uses: actions/upload-artifact@v7

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.prepare.outputs.ref
. (
workflow_dispatch
).
with:
name: web-dist
path: apps/web/dist
Expand All @@ -88,7 +109,7 @@
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:
Expand All @@ -106,17 +127,17 @@
# Shown by `repodna version --json`.
shell: bash
run: echo "REPODNA_BUILD_COMMIT=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
- name: Build
env:
# Link the C runtime statically so the Windows binary needs no Visual C++ runtime.
RUSTFLAGS: ${{ contains(matrix.target, 'windows-msvc') && '-C target-feature=+crt-static' || '' }}
run: cargo build --release --locked -p repodna-cli --target ${{ matrix.target }}
- name: Check the binary

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.prepare.outputs.ref
. (
workflow_dispatch
).
# The x86_64 macOS binary is cross-compiled on an Arm runner.
if: matrix.target != 'x86_64-apple-darwin'
shell: bash
run: ./target/${{ matrix.target }}/release/repodna --version
- name: Package

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.prepare.outputs.ref
. (
workflow_dispatch
).
shell: bash
env:
VERSION: ${{ needs.prepare.outputs.version }}
Expand Down Expand Up @@ -161,7 +182,7 @@
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: |
Expand All @@ -171,13 +192,21 @@
with:
node-version: 22
cache: npm
- run: npm ci
- uses: dtolnay/rust-toolchain@stable

Check failure

Code scanning / CodeQL

Cache Poisoning via execution of untrusted code High

Potential cache poisoning in the context of the default branch due to privilege checkout of untrusted code from
needs.prepare.outputs.ref
. (
workflow_dispatch
).
with:
targets: ${{ matrix.rust-targets }}
# 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
Comment on lines +200 to +202
# 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: |
Expand All @@ -194,16 +223,89 @@
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-*
Expand All @@ -225,9 +327,11 @@
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'
Expand All @@ -239,10 +343,13 @@
- `RepoDNA_<version>_amd64.deb`, `RepoDNA-<version>-1.x86_64.rpm`,
`RepoDNA_<version>_universal.dmg`, `RepoDNA_<version>_x64_en-US.msi`, and
`RepoDNA_<version>_x64-setup.exe`: the desktop app.
- `ghcr.io/sanskarin/repodna:<version>`: 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
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>/$VERSION/g" notes.md
cat notes.md
- name: Create or update the release
env:
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`).
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.** <https://sanskarin.github.io/RepoDNA/> 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.
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
],
"macOS": {
"signingIdentity": "-"
}
}
}
5 changes: 2 additions & 3 deletions crates/repodna-app/tests/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 4 additions & 3 deletions docs/desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
8 changes: 7 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
33 changes: 29 additions & 4 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -57,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.
The binaries and installers are not signed with a developer certificate.

- **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.
- **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**.

Expand All @@ -70,6 +74,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:
Expand Down
Loading
Loading