No description
  • C# 81.6%
  • HTML 14.4%
  • CSS 2.2%
  • JavaScript 1.5%
  • Dockerfile 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Nathan van Dael 288517831b
All checks were successful
Auto Build & Push Multi-Arch Docker Image / build-and-push (push) Successful in 4m6s
feat: Enhance OpenID configuration and add title display options for notifications
- Added `Enabled` property to OpenIdOptions to toggle OpenID Connect functionality.
- Updated IsConfigured logic to account for the new Enabled property.
- Introduced `ShowTitle` option in Telegram, Smtp, and Webhook settings to control title display in notifications.
- Added migration scripts to support the new `ShowTitle` column in relevant tables.
- Implemented tests to verify the behavior of title display settings across different notification types.
2026-10-05 16:53:33 +02:00
.claude
.forgejo/workflows
src feat: Enhance OpenID configuration and add title display options for notifications 2026-10-05 16:53:33 +02:00
tests/RepoWatch.Tests feat: Enhance OpenID configuration and add title display options for notifications 2026-10-05 16:53:33 +02:00
.dockerignore
.env.example feat: Enhance OpenID configuration and add title display options for notifications 2026-10-05 16:53:33 +02:00
.gitignore
docker-compose.yml
Dockerfile
dotnet-tools.json
LICENSE
README.md feat: Enhance OpenID configuration and add title display options for notifications 2026-10-05 16:53:33 +02:00
RepoWatch.slnx

RepoWatch

Self-hosted overview of the latest releases of repositories on GitHub, GitLab, Codeberg, Bitbucket and your own Forgejo, Gitea or GitLab servers, and of images on Docker Hub, Quay.io and ghcr.io.

  • Overview (/): all tracked projects with their latest version, newest release first. Releases from the last 7 days are labelled new. With Docker connected, a Current version column, before the latest version, shows the version that runs (on the admin page too), with update when it is behind. When the latest release has release notes, an icon after the version opens them in a dialog: fetched from GitHub, GitLab, Codeberg, Forgejo or Gitea when opened and kept for an hour. The Markdown is shown safely: HTML in it (badges, collapsible sections, tables) is kept but filtered, so there are no scripts, event handlers, styles, forms or frames, and only http(s) and mailto links. Images are shown, loaded only when scrolled into view and without a Referer; for that the page allows images over https from other sites. Clicking a project name opens its recent versions (up to 30 releases, or tags when there are none; up to 100 tags for images), grouped by date and linked to their release page where known; the list is fetched when opened and kept for 15 minutes. The overview reloads by itself when its data changes, for instance after an automatic check found a new release (checked every 30 seconds while the tab is visible, keeping the filter and scroll position). Under the list it says when the data was last fetched by the automatic check; checks started by hand do not count. The admin page shows the same under its project list and on the Fetching tab. Open to anyone, unless VIEWER_PASS is set.
  • Admin (/Admin): add and remove projects, check them manually (one, a selection or all; the checkbox in the header selects all projects shown by the filter, and a selection can also be deleted at once), link projects for the same software (such as a repository and its image on ghcr.io) into one, with a primary project that sets the name, version, release notes and notifications; the others are still checked, shown under it on the overview with their own version and a tab each in the versions dialog, but never notified, set up automatic checks, API tokens and servers, and the branding.

On the admin page, the Fetching tab lets you check all projects automatically. This is on by default; the default interval is 6 hours (1 minute to 7 days). Both can also be fixed with REFRESH_ENABLED and REFRESH_INTERVAL_MINUTES. The same tab holds the API tokens for GitHub, GitLab, Codeberg, Bitbucket, Docker Hub and Quay.io, and your own Forgejo, Gitea and GitLab servers. Tokens are stored encrypted and only ever shown masked.

Notifications

The Notifications tab on the admin page sends a message when a project has a new version compared to the previous check, by e-mail (SMTP), Telegram, Slack, Discord and/or a generic webhook. All new releases of one check run come in one message; each method has its own checkbox to send a message per release instead, also settable with SMTP_MODE, TELEGRAM_MODE, SLACK_MODE, DISCORD_MODE and WEBHOOK_MODE. A message with a single release is short: the site name with the release below it, without the intro or bullets. When a release has release notes (a release with a description on GitHub, GitLab, Codeberg, Forgejo or Gitea; not tags or images), a Release notes link follows: at the bottom of a message of its own, or after its line in a list; the generic webhook gets it as releaseNotesUrl. The first check of a new or imported project sends nothing. Notifications are sent in the background, so a check does not wait for them; what is still waiting to be sent is lost when the container restarts. Each method has a button to save and send a test, and shows how the last attempt went. Everything can also be set with the environment variables below; what is set there is shown but cannot be changed on the page.

For Telegram, create a bot with @BotFather, send it a message (or add it to a group or channel), and take the chat ID from https://api.telegram.org/bot<token>/getUpdates. Slack and Discord use an incoming webhook of the channel; services with Slack-compatible webhooks, such as Mattermost, work too.

The generic webhook is for n8n, Node-RED, Home Assistant or a script of your own. It gets a POST with this JSON body; the X-RepoWatch-Event header says new_releases or test:

{
  "event": "new_releases",
  "site": "RepoWatch",
  "text": "These projects have a new release:\ngrafana/grafana: v12.0.0 → v12.1.0",
  "releases": [{
    "project": "grafana/grafana",
    "projectUrl": "https://github.com/grafana/grafana",
    "previousVersion": "v12.0.0",
    "version": "v12.1.0",
    "releaseUrl": "https://github.com/grafana/grafana/releases/tag/v12.1.0",
    "releaseNotesUrl": "https://github.com/grafana/grafana/releases/tag/v12.1.0"
  }]
}

With a secret, every message also carries X-RepoWatch-Signature: sha256=<hex>: the HMAC-SHA256 of the raw body with the secret, the same scheme GitHub and Forgejo use for their webhooks.

Languages

The interface is available in English, Dutch and German. By default RepoWatch follows the browser language; the globe icon in the header lets you pick a language yourself, which is remembered in a cookie for a year. When neither is supported, the language is English.

The translations are in src/RepoWatch/i18n/{en,nl,de}.json, one file per language. A test checks that all files have the same keys and placeholders.

Branding

RepoWatch comes with a generic logo, generic colours and a generic footer. Change them on the admin page under Branding:

  • Site name: shown in the header, the browser tab and the default footer. Defaults to RepoWatch.
  • Logo: PNG, JPEG, GIF, WebP or SVG, at most 1 MB. Shown in the header. Without a logo of your own, a generic RepoWatch icon is shown. The favicon (32×32) and the home screen icon (apple-touch-icon, 180×180 on white) are derived from the logo automatically, keeping its proportions.
  • Colours: per mode (light and dark) the header, the accent colour (links, buttons, switches) and the text on it, the page, the cards and the text. Changes to the mode you are viewing are previewed right away; Restore default colours goes back to the defaults.
  • Footer: free text. {year} becomes the current year (e.g. © 1996-{year}), [text](https://example.com) becomes a link. Leaving it empty gives the default footer © {year} <site name>.

Supported input

Source Examples
GitHub https://github.com/grafana/grafana (sub paths such as /releases are fine)
GitLab https://gitlab.com/gitlab-org/gitlab-runner, https://gitlab.com/group/subgroup/project/-/releases (subgroups are fine)
Codeberg https://codeberg.org/forgejo/forgejo (sub paths such as /releases are fine)
Bitbucket https://bitbucket.org/workspace/repository
Self-hosted https://git.example.com/owner/repo, for every Forgejo, Gitea or GitLab server of your own set up on the admin page (Fetching tab), or with FORGEJO_URL or GITLAB_SERVER_URL
Docker Hub https://hub.docker.com/r/linuxserver/sonarr, https://hub.docker.com/_/nginx, nginx, grafana/grafana:latest
Quay.io https://quay.io/repository/prometheus/prometheus, quay.io/prometheus/prometheus:latest
ghcr.io ghcr.io/linuxserver/sabnzbd:latest, https://ghcr.io/linuxserver/sabnzbd, or the package page on GitHub (https://github.com/orgs/linuxserver/packages/container/package/sabnzbd)

Switching from NewReleases.io? The Projects tab on the admin page imports the projects you follow there: create an API key at newreleases.io/settings/api-keys and paste it in. The key is only used for the import and not saved. Projects from sources RepoWatch does not support (such as npm or PyPI) are skipped, as are Gitea projects on servers not set up here; the report lists them. Versions of imported projects are fetched at the next check.

How the latest version is determined:

  • GitHub, GitLab, Codeberg and Forgejo / Gitea: the latest release (drafts, pre-releases and upcoming releases do not count). When a repository has no releases, the newest tag.
  • Bitbucket: the newest tag, as Bitbucket has no releases.
  • Docker Hub and Quay.io: the highest plain version tag (1.2.3 or v1.2.3) among the 100 most recently updated tags. When there is none, the most recent tag that is not a floating tag such as latest or stable.
  • ghcr.io: the highest plain version tag (1.2.3 or v1.2.3) among all tags, with the date the image was built. The registry lists tags alphabetically without dates, so an image without version tags (only latest, main, …) has no release. Public images need no token; for private ones GITHUB_TOKEN is used, which then has to be a classic token with read:packages.

Running with Docker

cp .env.example .env
# Fill in ADMIN_USER and ADMIN_PASS
docker compose up -d --build

The app is then available at http://localhost:8080. Everything you can change lives in DATA_DIR (/app/data in the container, the repowatch-data volume):

Path Contents
repowatch.db SQLite database with the tracked projects and the settings (site name, footer, colours, automatic checks, encrypted tokens, self-hosted servers, notifications, login attempts)
branding/logo-*.{png,…} Uploaded logo
keys/ Keys for the login cookies and the encrypted tokens; without them, saved tokens have to be entered again

Storing the data in a folder on the host

The app does not run as root: by default it runs as the image's app user, UID 1654. It must be able to write to DATA_DIR; otherwise it stops at startup with SQLite Error 14: 'unable to open database file'. A named volume, as in docker-compose.yml, takes care of that itself. A folder on the host (a bind mount) keeps the owner it has on the host, which is root when it was created with sudo, so the app cannot write to it.

Mount the folder on /app/data, not on /data: that is where the app writes. Then either give the folder to UID 1654:

sudo chown -R 1654:1654 /path/on/host

or run the container as a user of your own that owns the folder, with user directly under the service:

services:
  repowatch:
    image: repowatch
    user: "1000:1000"   # the owner of the folder; check with: ls -lnd /path/on/host
    volumes:
      - /path/on/host:/app/data
    ports:
      - "8080:8080"

user is a key of the service, not an environment variable: user=1000:1000 under environment has no effect. The PUID/PGID variables that some images support are not used by RepoWatch either.

With Docker Swarm, the folder has to exist, with the same owner, on every node the service can be placed on; restart is ignored there, use deploy.restart_policy instead.

Behind a reverse proxy

RepoWatch only believes X-Forwarded-For and X-Forwarded-Proto from the addresses in TRUSTED_PROXIES. That has to be the address the request comes from, as RepoWatch sees it, which is not always the proxy's own address:

  • Plain Docker or Compose, with the proxy (such as Nginx Proxy Manager) in a network shared with RepoWatch: the proxy's address in that network, or the network, e.g. 172.18.0.0/16.
  • Docker Swarm with a published port (ports: 40004:8080): requests pass the ingress routing mesh, which replaces the sender with an address in the ingress network (often 10.0.0.0/24). Trusting that network works, but then everyone who can reach the published port directly could send their own X-Forwarded-For. Better: put the proxy and RepoWatch in a shared overlay network, leave out ports, let the proxy forward to http://<stack>_repowatch:8080, and trust that overlay network.

When a request carries X-Forwarded-For from an address that is not trusted, RepoWatch logs it once, for example X-Forwarded-For van 10.0.0.2 wordt genegeerd; that is the address (or network) to add. With LOG_REQUESTS=true every request shows both the client and the sender. The Audit tab shows which address logins came from.

Connecting a Docker host

RepoWatch can read which containers run on a Docker host, to show on the overview what runs next to the latest version (runs 5.1.0 · update) and, if you like, to add their images as projects. It only reads containers and images from the Docker Engine API; it never starts, stops or changes anything.

There are two ways, both without other images:

  • Through the RepoWatch agent (recommended): a small image of its own, repowatch-agent, next to RepoWatch. Only the agent has the Docker socket; RepoWatch asks it what runs, with a token. The agent never passes on Docker API requests, only names, images, versions and (on a Swarm) replicas and nodes, so not, for instance, the environment variables of containers with their passwords. The same agent serves a Swarm, see below. The docker-compose.yml in this repository runs RepoWatch this way, for trying it locally.
  • Directly: mount the socket into the RepoWatch container. At its usual place, /var/run/docker.sock, it is found by itself; DOCKER_HOST is only needed for another path or a Docker daemon over TCP (tcp://host:2375). The socket belongs to the docker group of the host, so add that group to the container (getent group docker shows its number); without it the Docker tab says that there is no access to the socket. Keep in mind that access to the socket is as good as root on the host, also with :ro.
# The agent next to RepoWatch, on a single Docker host.
services:
  repowatch:
    image: git.example.com/you/repowatch:latest
    environment:
      - DOCKER_AGENT_URL=http://repowatch-agent:8080
      - DOCKER_AGENT_TOKEN=${AGENT_TOKEN}
  repowatch-agent:
    image: git.example.com/you/repowatch-agent:latest
    environment:
      - AGENT_TOKEN=${AGENT_TOKEN}     # at least 16 characters
      - DOCKER_HOST_NAME=nas
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

The agent runs as root, so it can read the socket on any host. It only reads from Docker and only answers with its token; to run it as an ordinary user instead, set user: "1654" with group_add for the docker group of the host. It listens on port 8080 and need not be published: RepoWatch reaches it in the network they share. /healthz answers for health checks. Its settings are AGENT_TOKEN (required), DOCKER_HOST (the mounted socket by default) and DOCKER_HOST_NAME.

The Docker tab on the admin page shows each source (the own host, each agent) with how the last read went, and the running containers: the image and version each runs and the project it belongs to, with a button to start tracking an image that is not tracked yet, and a switch to add them all automatically. They are read at every automatic check and with Read now. What runs is the tag when that is a version (nginx:1.27.2), otherwise the image's org.opencontainers.image.version label (for :latest); it counts as behind when its numbers are lower than the latest version (5.1.0-ls270 runs 5.1.0). lscr.io/linuxserver/… images are tracked as the same images on ghcr.io; images from registries RepoWatch does not support are listed but not tracked.

Connecting a Docker Swarm

On a Swarm, the services and the nodes their tasks run on are only known to a manager, so the agent runs there. On a manager it reads the services of the whole Swarm instead of the containers; on the overview and the Docker tab a service shows how many of its replicas run, and on which nodes.

# The agent on a manager; RepoWatch reaches it over the overlay network they share.
services:
  repowatch-agent:
    image: git.example.com/you/repowatch-agent:latest
    environment:
      - AGENT_TOKEN=${AGENT_TOKEN}
      - DOCKER_HOST_NAME=swarm
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks: [repowatch]
    deploy:
      placement:
        constraints: [node.role == manager]

networks:
  repowatch:
    driver: overlay
    attachable: true

Then set DOCKER_AGENT_URL=http://repowatch-agent:8080 (or http://<stack>_repowatch-agent:8080) and DOCKER_AGENT_TOKEN for RepoWatch, in the same network. More Swarms or hosts take DOCKER_AGENT_URL_2 / DOCKER_AGENT_TOKEN_2 and so on.

Environment variables

Variable Required Description Default
ADMIN_USER Yes Username for the admin page. admin
ADMIN_PASS Yes¹ Password for the admin page. Without a password nobody can log in. ¹Not with OpenID Connect (OIDC_AUTHORITY), which replaces it. –
VIEWER_USER No Username for the overview. viewer
VIEWER_PASS No Password for the overview. When set, the overview needs a login (as viewer or as admin); when not set, anyone can see it. –
GITHUB_TOKEN No GitHub token (no scopes needed). Raises the API limit from 60 to 5000 requests per hour. Also used for private images on ghcr.io; that takes a classic token with read:packages (fine-grained tokens do not work there; public images keep working without). Can also be set on the admin page; when set here, it is shown there (masked) but cannot be changed. –
GITLAB_TOKEN No GitLab personal access token with the read_api scope, for private projects and higher limits. A wrong token also blocks public projects. Works like GITHUB_TOKEN. –
CODEBERG_TOKEN No Codeberg token with read access to repositories. Only needed for private repositories. Works like GITHUB_TOKEN. –
FORGEJO_URL No Your own Forgejo or Gitea server, e.g. https://git.example.com (a sub path is allowed). More servers with FORGEJO_URL_2, FORGEJO_URL_3, … (up to _20). Servers can also be added on the admin page; those from the environment are shown there but cannot be changed. –
FORGEJO_TOKEN No Token for FORGEJO_URL, FORGEJO_TOKEN_2 for FORGEJO_URL_2 and so on. Each token is only sent to its own server. –
QUAY_TOKEN No OAuth access token of a Quay.io application that may view repositories (Organization → Applications → Generate Token). Only needed for private images; robot accounts do not work with the API RepoWatch uses. Works like GITHUB_TOKEN. –
BITBUCKET_USERNAME No Atlassian e-mail address (or username) for BITBUCKET_TOKEN. Leave it empty for a workspace or repository access token. Works like GITHUB_TOKEN. –
BITBUCKET_TOKEN No Bitbucket API token with read access to repositories, or a workspace or repository access token. For private repositories and more than 60 requests per hour. Works like GITHUB_TOKEN. –
GITLAB_SERVER_URL No Your own GitLab server, e.g. https://gitlab.example.com (a sub path is allowed). More servers with GITLAB_SERVER_URL_2, … (up to _20). Works like FORGEJO_URL. –
GITLAB_SERVER_TOKEN No Personal access token with the read_api scope for GITLAB_SERVER_URL, GITLAB_SERVER_TOKEN_2 for GITLAB_SERVER_URL_2 and so on. Each token is only sent to its own server. –
DOCKERHUB_USERNAME No Docker Hub user of DOCKERHUB_TOKEN. Works like GITHUB_TOKEN. –
DOCKERHUB_TOKEN No Docker Hub personal access token (read-only). Needed for private images and gives higher rate limits. Works like GITHUB_TOKEN. –
REFRESH_ENABLED No true or false (or 1/0): switches automatic checks on or off. When set, the switch on the admin page is shown but cannot be changed. –
REFRESH_INTERVAL_MINUTES No Minutes between automatic checks (1 to 10080); without it the admin page decides, 6 hours (360) by default. When set, the interval on the admin page is shown but cannot be changed. –
NOTIFICATIONS_LANGUAGE No Language of the notifications: en, nl or de. en
SMTP_ENABLED No true or false: send notifications by e-mail. –
SMTP_HOST No SMTP server, e.g. smtp.example.com. –
SMTP_PORT No SMTP port. 587
SMTP_SECURITY No auto, starttls, ssl or none. auto uses SSL/TLS on port 465 and STARTTLS when the server offers it otherwise. auto
SMTP_USERNAME No Username for the SMTP server; leave it empty for a server without login. –
SMTP_PASSWORD No Password for SMTP_USERNAME. –
SMTP_FROM No Sender, e.g. RepoWatch <repowatch@example.com>. –
SMTP_TO No One or more recipients, separated by commas. –
TELEGRAM_ENABLED No true or false: send notifications through Telegram. –
TELEGRAM_BOT_TOKEN No Token of the bot, from @BotFather. –
TELEGRAM_CHAT_ID No Chat to send to: a number (negative for groups and channels) or @channelname. –
TELEGRAM_MODE No list: all new releases of a check in one message. individual: a message per release, with a pause in between to stay within Telegram's limits; when Telegram still says there are too many, RepoWatch waits as long as it asks and sends again. The same for SMTP_MODE (e-mail), SLACK_MODE, DISCORD_MODE and WEBHOOK_MODE (generic webhook: a POST per release). list
TELEGRAM_SHOW_TITLE No true or false: start each Telegram message with the site name in bold. The same for SLACK_SHOW_TITLE and DISCORD_SHOW_TITLE, and SMTP_SHOW_TITLE for the site name at the start of the e-mail subject. When set, the checkbox on the admin page is shown but cannot be changed. true
SLACK_ENABLED No true or false: send notifications to Slack. –
SLACK_WEBHOOK_URL No Incoming webhook URL of the Slack channel (or of a Slack-compatible service). –
DISCORD_ENABLED No true or false: send notifications to Discord. –
DISCORD_WEBHOOK_URL No Webhook URL of the Discord channel. –
WEBHOOK_ENABLED No true or false: send notifications to the generic webhook. –
WEBHOOK_URL No URL that gets the JSON above (http or https). –
WEBHOOK_SECRET No Optional key to sign each message with (X-RepoWatch-Signature). –
TRUSTED_PROXIES No Comma separated addresses or networks of your reverse proxy, e.g. 172.16.0.0/12 for a Docker network. Their X-Forwarded-For and X-Forwarded-Proto headers are then believed, so the audit log and the login blocking see the visitor's own IP address. Without it these headers are ignored, as anyone could send them. –
REMEMBER_LOGIN_DAYS No How many days Stay logged in on the login page lasts, counted from the last visit (0 to 365). 0 leaves the option out, so every login ends when the browser is closed. 7
LOG_REQUESTS No true logs one line per page or action on the console: method, path, status, duration and client address, plus the sender when that differs (via 10.0.0.2, the reverse proxy). Static files are only logged when they fail. Meant for finding problems, for instance with a reverse proxy; open overviews poll every 30 seconds, so it is chatty. false
DOCKER_HOST No The Docker Engine API to read the running containers from directly: unix:///path/to/docker.sock or tcp://host:2375. Not needed with an agent; none switches the direct connection off, also when the socket is mounted. See Connecting a Docker host. the socket at /var/run/docker.sock, when it is mounted
DOCKER_HOST_NAME No Name of the Docker host on the pages. machine name
DOCKER_AGENT_URL No A RepoWatch agent, e.g. http://repowatch-agent:8080, to read what runs on a Swarm. More with DOCKER_AGENT_URL_2, … (up to _20). See Connecting a Docker Swarm. –
DOCKER_AGENT_TOKEN No The AGENT_TOKEN of the agent of DOCKER_AGENT_URL; DOCKER_AGENT_TOKEN_2 for DOCKER_AGENT_URL_2 and so on. –
DOCKER_AUTO_ADD No true or false: add images of running containers as projects automatically. When set, the switch on the admin page is shown but cannot be changed. –
OIDC_ENABLED No false switches OpenID Connect off while keeping its other settings; the built-in login (ADMIN_*, VIEWER_*) works again. true when OIDC_AUTHORITY and OIDC_CLIENT_ID are set
OIDC_AUTHORITY No Issuer of the OpenID Connect provider, e.g. https://auth.example.com/application/o/repowatch/ for Authentik. Together with OIDC_CLIENT_ID it switches the built-in login off. See Single sign-on with OpenID Connect. –
OIDC_CLIENT_ID No Client ID at the provider. –
OIDC_CLIENT_SECRET No Client secret at the provider (a confidential client). –
OIDC_ADMIN_CLAIM No The claim that decides who is admin, such as an attribute of the user or groups. repowatch_role
OIDC_ADMIN_VALUE No The value of OIDC_ADMIN_CLAIM that makes someone admin (case does not matter), e.g. a group name, or true for a claim such as is_admin: true. admin
OIDC_VIEWER_VALUE No When set, only who has this value of OIDC_VIEWER_CLAIM (or is admin) may log in; otherwise anyone the provider lets in is viewer. –
OIDC_VIEWER_CLAIM No The claim for OIDC_VIEWER_VALUE. OIDC_ADMIN_CLAIM
OIDC_SCOPES No Scopes to ask for, separated by spaces or commas; openid is always added. Add the scope that carries OIDC_ADMIN_CLAIM, such as repowatch. openid profile email
OIDC_NAME No Name on the login button. OpenID Connect
OIDC_PUBLIC_URL No The address RepoWatch is reached at, for the redirect back from the provider, e.g. https://repowatch.example.com. Without it the address of the request is used. –
OIDC_ANONYMOUS_OVERVIEW No true leaves the overview open to anyone; otherwise it needs a login. false
TZ No Time zone for the times shown, e.g. Europe/Amsterdam. –
DATA_DIR No Directory for the database, the uploaded logo and the keys. data

Copy .env.example to .env and fill in the values before starting the container.

Tokens for Codeberg, Forgejo and Gitea

RepoWatch only reads releases and tags (/api/v1/repos/{owner}/{repo}/releases and /tags), so a token needs read access to repositories and nothing else. For public repositories no token is needed at all; one only helps for private repositories, and against rate limits.

To create one on Codeberg or on your own Forgejo server:

  1. Open Settings → Applications (https://codeberg.org/user/settings/applications, or /user/settings/applications on your own server).
  2. Under Generate new token, enter a name, such as RepoWatch.
  3. Repository and Organization Access: choose Public only when you only follow public repositories, or All (public, private and limited) for private ones, including those of organizations you are a member of.
  4. Select permissions: set repository to Read. Leave all other permissions on No access; RepoWatch needs no access to organizations, issues, packages or your account.
  5. Click Generate token and copy the token; it is shown only once.
  6. Enter it on the admin page (Fetching tab), or set it with CODEBERG_TOKEN (Codeberg) or FORGEJO_TOKEN / FORGEJO_TOKEN_n (your own server).

On Gitea (1.19 or later) it works the same way: Settings → Applications, permission repository: Read. Older Gitea versions have no permissions, so their tokens always have full access to the account; use a separate account with access only to the repositories you want to follow.

When a private repository is still not found with a token, check that the token's account can see the repository in the browser, and that the token was created with All rather than Public only.

Logging in

There is one login page, /Login, for two roles:

Role Credentials May see
Admin ADMIN_USER / ADMIN_PASS the admin page (/Admin) and the overview
Viewer VIEWER_USER / VIEWER_PASS the overview, and only when VIEWER_PASS is set; otherwise the overview is open to anyone

Every login attempt, including failed and blocked ones, is kept for 90 days: the Audit tab on the admin page shows who logged in, with which role, from which IP address and when.

A login lasts 12 hours from the last visit and ends when the browser is closed. With Stay logged in on the login page it also survives closing the browser, and lasts 7 days from the last visit: every visit starts the 7 days again. REMEMBER_LOGIN_DAYS sets the number of days; 0 leaves the option out.

A viewer who opens the admin page is asked to log in as admin. The login page, the logo, the favicons, the colours and the language switch are always available, as the login page needs them.

Security:

  • Login cookie (HttpOnly, SameSite=Strict) that expires when the browser is closed, or after 12 hours of inactivity
  • CSRF tokens on all forms
  • After 5 failed attempts, logging in from that IP address is blocked for 5 minutes
  • Security headers (Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy)

Single sign-on with OpenID Connect (Authentik)

Instead of the built-in login, RepoWatch can let an identity provider log people in, such as Authentik. Once OIDC_AUTHORITY and OIDC_CLIENT_ID are set (and OIDC_ENABLED is not false), the login page only has a Log in with … button: ADMIN_USER/ADMIN_PASS and VIEWER_USER/VIEWER_PASS no longer work. Who is admin follows from a claim: by default repowatch_role with the value admin, from an attribute of the user, the way Immich does it with immich_role. Everyone else who gets in is viewer.

In Authentik:

  1. Customization → Property Mappings → Create, Scope Mapping: name RepoWatch role, scope name repowatch, and as expression:
    return {"repowatch_role": request.user.attributes.get("repowatch_role", "user")}
    
  2. Applications → Providers → Create, OAuth2/OpenID Provider: client type Confidential, the authorization flow you use, and as Redirect URIs (strict) https://repowatch.example.com/signin-oidc and https://repowatch.example.com/signout-callback-oidc. Under Advanced protocol settings → Scopes, keep openid, email and profile and add RepoWatch role.
  3. Applications → Applications → Create: name and slug repowatch, with that provider. Optionally bind groups or a policy so only some may log in.
  4. For each admin, Directory → Users → Edit → Attributes: repowatch_role: admin.
  5. Set the variables below; OIDC_SCOPES must ask for the repowatch scope of step 1. The issuer is the OpenID Configuration Issuer of the provider, ending in /application/o/<slug>/.
environment:
  - OIDC_AUTHORITY=https://auth.example.com/application/o/repowatch/
  - OIDC_CLIENT_ID=<client ID of the provider>
  - OIDC_CLIENT_SECRET=<client secret>
  - OIDC_SCOPES=openid profile email repowatch
  - OIDC_NAME=Authentik
  - OIDC_PUBLIC_URL=https://repowatch.example.com

A group works as well: OIDC_ADMIN_CLAIM=groups and OIDC_ADMIN_VALUE=RepoWatch Admins; Authentik's profile scope already holds the groups, so no scope mapping is needed then.

The redirect back must be the public address. Behind a reverse proxy, either set OIDC_PUBLIC_URL, or TRUSTED_PROXIES so RepoWatch sees https and the right host; otherwise the provider refuses the redirect URI. Logging out in RepoWatch also ends the session at the provider. Logins through the provider are in the Audit tab like any other, including those refused because of OIDC_VIEWER_VALUE. Any OpenID Connect provider with the code flow works the same way (Keycloak, Authelia, Zitadel, …).

Development

Requires the .NET 10 SDK.

The solution has three projects: src/RepoWatch (the app), src/RepoWatch.Agent (the agent for a Swarm, an image of its own: src/RepoWatch.Agent/Dockerfile) and src/RepoWatch.Docker (reading from the Docker Engine API, shared by both). docker compose up -d --build starts RepoWatch with the agent on the local Docker, to try the Docker pages.

ADMIN_PASS=secret dotnet run --project src/RepoWatch     # http://localhost:5213
dotnet test
AGENT_TOKEN=local-test-token-change-me dotnet run --project src/RepoWatch.Agent   # the agent, with the local Docker socket

After changing the data model, add a migration:

dotnet tool restore
dotnet ef migrations add <Name> --project src/RepoWatch --output-dir Data/Migrations

Migrations are applied automatically on startup.