- C# 81.6%
- HTML 14.4%
- CSS 2.2%
- JavaScript 1.5%
- Dockerfile 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Auto Build & Push Multi-Arch Docker Image / build-and-push (push) Successful in 4m6s
- 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. |
||
| .claude | ||
| .forgejo/workflows | ||
| src | ||
| tests/RepoWatch.Tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| dotnet-tools.json | ||
| LICENSE | ||
| README.md | ||
| 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 aReferer; 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, unlessVIEWER_PASSis 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.3orv1.2.3) among the 100 most recently updated tags. When there is none, the most recent tag that is not a floating tag such aslatestorstable. - ghcr.io: the highest plain version tag (
1.2.3orv1.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 (onlylatest,main, …) has no release. Public images need no token; for private onesGITHUB_TOKENis used, which then has to be a classic token withread: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 theingressnetwork (often10.0.0.0/24). Trusting that network works, but then everyone who can reach the published port directly could send their ownX-Forwarded-For. Better: put the proxy and RepoWatch in a shared overlay network, leave outports, let the proxy forward tohttp://<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. Thedocker-compose.ymlin 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_HOSTis only needed for another path or a Docker daemon over TCP (tcp://host:2375). The socket belongs to thedockergroup of the host, so add that group to the container (getent group dockershows 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:
- Open Settings → Applications (
https://codeberg.org/user/settings/applications, or/user/settings/applicationson your own server). - Under Generate new token, enter a name, such as
RepoWatch. - 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.
- Select permissions: set repository to Read. Leave all other permissions on No access; RepoWatch needs no access to organizations, issues, packages or your account.
- Click Generate token and copy the token; it is shown only once.
- Enter it on the admin page (Fetching tab), or set it with
CODEBERG_TOKEN(Codeberg) orFORGEJO_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:
- Customization → Property Mappings → Create, Scope Mapping: name
RepoWatch role, scope namerepowatch, and as expression:return {"repowatch_role": request.user.attributes.get("repowatch_role", "user")} - Applications → Providers → Create, OAuth2/OpenID Provider: client type Confidential, the authorization flow you use, and as Redirect URIs (strict)
https://repowatch.example.com/signin-oidcandhttps://repowatch.example.com/signout-callback-oidc. Under Advanced protocol settings → Scopes, keepopenid,emailandprofileand addRepoWatch role. - Applications → Applications → Create: name and slug
repowatch, with that provider. Optionally bind groups or a policy so only some may log in. - For each admin, Directory → Users → Edit → Attributes:
repowatch_role: admin. - Set the variables below;
OIDC_SCOPESmust ask for therepowatchscope 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.