5 |
--------------------------------------------------------------------------------
/docs/structure.md:
--------------------------------------------------------------------------------
1 | ## Structure
2 |
3 | - `./reschedule.py` - a tool for users of the janitor and can be run by anybody locally
4 | - `./helpers/*` - all need to run inside of a janitor deployment (and talk to the database, etc) by an admin.
5 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | credentials.*
2 | *.secret
3 | .mypy_cache
4 | .pytest_cache
5 | .ruff_cache
6 | .git
7 | .bzr
8 | .eggs
9 | *~
10 | target/
11 | k8s
12 | *.conf
13 | bin/
14 | htmlcov/
15 | tests/
16 | build/
17 | lib/
18 | include/
19 | .venv
20 |
--------------------------------------------------------------------------------
/py/janitor/site/templates/result-codes/timeout.html:
--------------------------------------------------------------------------------
1 |
2 | The build of the package timed out. This often means that it is stuck in an
3 | endless loop or did not generate output to standard out for a long time
4 | (usually an hour).
5 |
2 | There are changes between the upstream tarball and the non-debian/ part of
3 | the packaging repository that are not accounted for by any of the patches
4 | under debian/patches.
5 |
The worker failed to respond to pings, and the run was therefore marked as lost.
2 |
3 | In some cases, this can happen because the runner is rejecting the uploads
4 | from the worker - e.g. because it consistently crashes while processing them.
5 |
2 | The packaging branch is using a version control system that is not supported
3 | by the Janitor. At the moment, only Git and
4 | Bazaar repositories are supported.
5 |
The package needs a Go package to be installed in order to build.
2 |
3 | However, the Go package is not available in the APT repository for
4 | the distribution and can thus not be added to the build dependencies
5 | for the package that's being built.
6 |
One or more control files in the packaging branch are generated from another file.
2 |
3 | The Janitor identifies generated files by looking for the string
4 |
DO NOT EDIT
in control files, and the existence of files with the
5 | .in extension.
6 |
7 |
--------------------------------------------------------------------------------
/py/janitor/site/templates/result-codes/invalid-path-normalization.html:
--------------------------------------------------------------------------------
1 |
The package contains a path that isn't unicode normalized.
2 |
3 | Breezy currently prohibits this, because it results in issues checking out
4 | files on Mac OS X. See this bug
5 | about loosening the constraints.
6 |
7 |
--------------------------------------------------------------------------------
/docs/glossary.md:
--------------------------------------------------------------------------------
1 | * *codebase*: A collection of source code files that are managed together in a
2 | version control system. Usually this will be the root of a specific branch in a
3 | vcs repository. Sometimes, it will be a subdirectory in a VCS. It can also be
4 | e.g. a tarball somewhere.
5 |
6 | * *cotenants*: Other codebases that share the same branch as the current codebase.
7 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | Jelmer Vernooij
2 |
3 | There are a lot of people who contributed ideas and feedback to the Janitor.
4 | Some of them are listed here; if you're missing, please let me know!
5 |
6 | Thanks:
7 |
8 | Perry Lorrier
9 | Christoph Berg
10 | Raphael Hertzog
11 | Gregor Herrman
12 | Holger Levsen
13 | Helmut Grohne
14 | Mattia Rizzolo
15 | Colin Watson
16 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod analyze_log;
2 | pub mod api;
3 | pub mod artifacts;
4 | pub mod config;
5 | pub mod debdiff;
6 | pub mod logging;
7 | pub mod logs;
8 | pub mod prometheus;
9 | pub mod publish;
10 | pub mod queue;
11 | pub mod reprocess_logs;
12 | pub mod schedule;
13 | pub mod schema;
14 | pub mod state;
15 | pub mod vcs;
16 |
17 | /// The type of a run ID.
18 | pub type RunId = String;
19 |
--------------------------------------------------------------------------------
/py/janitor/site/templates/faq-incorrect.html:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/auto-upload/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Auto-upload crate for the Janitor project.
2 | //!
3 | //! This crate provides functionality for automatically uploading Debian packages.
4 |
5 | #![deny(missing_docs)]
6 |
7 | /// Re-export for signing Debian packages
8 | pub use silver_platter::debian::uploader::debsign;
9 |
10 | /// Re-export for uploading Debian changes files
11 | pub use silver_platter::debian::uploader::dput_changes;
12 |
--------------------------------------------------------------------------------
/py/janitor/site/templates/result-codes/branch-unavailable.html:
--------------------------------------------------------------------------------
1 |
2 | The packaging branch
3 | {% if vcs_url %}at ({{ vcs_url }}){% endif %}
4 | can not be
5 | reached.
6 |
7 |
This problem may be intermittent, e.g. if the hosting site is temporarily down.
8 |
9 | If the repository has moved, please update the Vcs-* headers
10 | in the package and upload a new version.
11 |
12 |
--------------------------------------------------------------------------------
/src/api/runner.rs:
--------------------------------------------------------------------------------
1 | /// Sent when the publish-status for a run changes.
2 | #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
3 | struct PublishStatusPubsub {
4 | /// The codebase.
5 | codebase: String,
6 |
7 | /// The run ID.
8 | run_id: crate::RunId,
9 |
10 | /// The new publish-status.
11 | #[serde(rename = "publish-status")]
12 | publish_status: crate::api::RunPublishStatus,
13 | }
14 |
--------------------------------------------------------------------------------
/site/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "janitor-site"
3 | version = "0.0.0"
4 | authors = ["Jelmer Vernooij "]
5 | edition.workspace = true
6 | description = "Basic site for the janitor"
7 | license = "GPL-3.0+"
8 | repository = "https://github.com/jelmer/janitor.git"
9 | homepage = "https://github.com/jelmer/janitor"
10 |
11 | [dependencies]
12 | buildlog-consultant = { version = "0.1.0", default-features = false }
13 |
--------------------------------------------------------------------------------
/py/janitor/site/templates/result-codes/run-disappeared.html:
--------------------------------------------------------------------------------
1 |
2 | The management plane for the worker no longer knows about the
3 | job that was processing the run. This can happen because e.g.
4 | the management plane for the worker was restarted, or because the worker
5 | failed to upload its results to the runner, possibly because the
6 | runner was down or rejected the uploads - it could be crashing
7 | while processing them.
8 |
The upstream branch could not be found; it may have been moved.
2 |
3 | The upstream branch will be taken from the Repository field in
4 | debian/upstream/metadata or guessed based on metadata in the source
5 | package.
6 |
7 |
8 | To fix this error, set the Repository field appropriately in
9 | debian/upstream/metadata.
10 |
2 | Attempting to access the packaging branch resulted in a HTTP
3 | 401 Unauthorized error. This can indicate that the repository is
4 | private, that accessing it may require log in, or that it simply does not
5 | exist.
6 |
7 |
8 | This can happen due to Breezy not sending credentials to GitLab
9 | repositories. See
10 | this bug
11 | for details.
12 |
16 | {% endblock body %}
17 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Please see the documentation for all configuration options:
2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
3 | ---
4 | version: 2
5 | updates:
6 | - package-ecosystem: "cargo"
7 | directory: "/"
8 | schedule:
9 | interval: "weekly"
10 | - package-ecosystem: "github-actions"
11 | directory: "/"
12 | schedule:
13 | interval: weekly
14 | - package-ecosystem: "pip"
15 | directory: "/"
16 | schedule:
17 | interval: "weekly"
18 |
--------------------------------------------------------------------------------
/docs/production.md:
--------------------------------------------------------------------------------
1 | # Running Janitor in production
2 |
3 | There are [containers](Dockerfiles_.md) available for each of the Janitor services.
4 |
5 | [pre-built containers](https://github.com/jelmer?tab=packages&repo_name=janitor) are
6 | available, but you can also create them yourself:
7 |
8 | ```console
9 | $ sudo apt install \
10 | buildah \
11 | make
12 | $ make build-all
13 | ```
14 |
15 | For a Janitor instance, you probably want a custom website in combination with
16 | the Janitor API. See the existing instances for inspiration.
17 |
--------------------------------------------------------------------------------
/py/janitor/debian/debian.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS debversion;
2 | CREATE TABLE debian_build (
3 | run_id text not null references run (id),
4 | -- Debian version text of the built package
5 | version debversion not null,
6 | -- Distribution the package was built for (e.g. "lintian-fixes")
7 | distribution text not null,
8 | source text not null,
9 | binary_packages text[],
10 | lintian_result json
11 | );
12 | CREATE INDEX ON debian_build (run_id);
13 | CREATE INDEX ON debian_build (distribution, source, version);
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/archive/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Archive crate for the Janitor project.
2 | //!
3 | //! This crate provides functionality for working with package archives.
4 |
5 | #![deny(missing_docs)]
6 |
7 | use tracing::{debug, error, info};
8 |
9 | /// Temporary prefix used for archive operations.
10 | pub const TMP_PREFIX: &str = "janitor-apt";
11 | /// Default timeout for Google Cloud Storage operations in seconds.
12 | pub const DEFAULT_GCS_TIMEOUT: usize = 60 * 30;
13 |
14 | /// Scanner module for archive operations.
15 | pub mod scanner;
16 |
17 | // TODO(jelmer): Generate contents file
18 |
--------------------------------------------------------------------------------
/py/janitor/site/templates/result-codes/upstream-branch-unknown.html:
--------------------------------------------------------------------------------
1 |
2 | The location of the upstream repository of this package is unknown. The canonical location
3 | for this information is the Repository field in the debian/upstream/metadata file.
4 |
5 |
6 | The Janitor will also fall back to attempting to reading various other metadata files
7 | to figure out the upstream repository location, such as dist.ini (for perl packages)
8 | or setup.py (for Python packages).
9 |
6 | The Janitor will automatically reschedule processing of packages with a
7 | conflicted merge proposal. Once a conflict appears, it may take a couple of
8 | hours before the merge proposal is updated.
9 |
10 |
11 | It will also regularly rebase merge proposals on the packaging branch. It
12 | can take several days before this happens, since there is no mechanism to
13 | notify the Janitor of new commits. You can manually trigger a rerun
14 | from the package-specific page linked from the merge proposal.
15 |
Simply give the bot commit access to your repository, and it will push fixes rather than proposing them.
7 |
8 | The bot will need permission to push to the relevant branches. For
9 | repositories on a GitLab instances (such as salsa) this means that
10 | it will need developer permissions if the relevant branch is unprotected,
11 | and maintainer permissions if the relevant branch is protected.
12 | See the GitLab permissions guide for details.
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/test_launchpad.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Copyright (C) 2022 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 |
19 | import janitor._launchpad # noqa: F401
20 |
--------------------------------------------------------------------------------
/archive/tests/data/hello_2.10.orig.tar.gz.asc:
--------------------------------------------------------------------------------
1 | -----BEGIN PGP SIGNATURE-----
2 | Version: GnuPG v2
3 |
4 | iQIcBAABCAAGBQJUaJPFAAoJEKlVMkX96bc5EuMP/2Z8T2r+ZjJbveEfVKuY3LbG
5 | sPqZqI/t0WISsfhRen3R/tiis0lN8TWIdTFRnLqtxyqfzDDtgzrPg8gwICFYxE6W
6 | ffVvgbbDA14EuatWHfnAK1SjWsaemJIO1rGROgNFqsatEDIOf7bg4NZ6Gs1QR0rJ
7 | p4W+LYKiP8UeJwV4Xd2d/h+rf4XBWo5HTNYwgZpZawklWupmIx0bXi3HiRs4MJQm
8 | mfbNrNE5YcAWQpBwAxgcUCwGHlvDonpvu0i0D4tNoMeneLAhZty1GCnamTlcuDXJ
9 | IZpg0Ky9mYEnyRhaRnLsyaZ2kzJhOSMNfVzSP2+ge+JfTuenw1yvhAZC9qTLoV+f
10 | 1xUhxUkmzgDV3pVpc9qB+LVGfJclrHtrgD2dakmph5JGGhAoAExwrkyO3qxE5jzJ
11 | x2C83aNpBjNqPhAVIcywXpFWBT8sbsXgwLufXWFwQtyxIm1dxrOku0SI5oYm1ZON
12 | l1rjkaQmpFKx1oo7eOG1XLbCQ1Ii1qEDSiXTvQwoaBTkAcPz1KOVGEyby6kf9AS9
13 | DjuJzh8oQgylaDk5FqGqsY6S90Naz7SJSrBi/3xOP51LvsLciNx+EPBNrRJzFDYw
14 | svn/ahaWx4hsXr0ErjqAzqE6ZNQQcyKa5qDDFD5dz14dSI78FjZ4u2WUwZCGyW+u
15 | OFOwPF0lXuPO6q2UFcpj
16 | =s6wC
17 | -----END PGP SIGNATURE-----
18 |
--------------------------------------------------------------------------------
/Dockerfile_mail_filter:
--------------------------------------------------------------------------------
1 | # https://hub.docker.com/_/debian
2 | FROM docker.io/debian:testing-slim AS build
3 | MAINTAINER Jelmer Vernooij
4 |
5 | ARG DEBIAN_FRONTEND=noninteractive
6 |
7 | RUN apt-get update --yes \
8 | && apt-get install --yes --no-install-recommends \
9 | auto-apt-proxy \
10 | iproute2 \
11 | && apt-get upgrade --yes \
12 | && apt-get satisfy --yes --no-install-recommends \
13 | ca-certificates \
14 | cargo \
15 | libpython3-dev \
16 | libssl-dev \
17 | pkg-config \
18 | protobuf-compiler \
19 | python3-minimal \
20 | && apt-get clean
21 |
22 | COPY . /code
23 |
24 | RUN cargo build --release --manifest-path /code/mail-filter/Cargo.toml
25 |
26 | FROM docker.io/debian:testing-slim
27 |
28 | COPY --from=build /code/target/release/janitor-mail-filter /usr/local/bin/janitor-mail-filter
29 |
30 | ENTRYPOINT ["janitor-mail-filter"]
31 |
--------------------------------------------------------------------------------
/differ-py/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "differ-py"
3 | version = "0.0.0"
4 | authors = ["Jelmer Vernooij "]
5 | edition.workspace = true
6 | description = "Differ for the janitor - python bindings"
7 | publish = false
8 | license = "GPL-3.0+"
9 | repository = "https://github.com/jelmer/janitor.git"
10 | homepage = "https://github.com/jelmer/janitor"
11 |
12 | [lib]
13 | crate-type = ["cdylib"]
14 |
15 | [dependencies]
16 | pyo3 = {workspace = true, features=["serde", "chrono"]}
17 | janitor-publish = { path = "../publish" }
18 | pyo3-log = { workspace = true }
19 | log = "0.4"
20 | chrono = { workspace = true, features = ["serde"] }
21 | breezyshim.workspace = true
22 | silver-platter = { workspace = true, features = ["debian"] }
23 | janitor-differ = { path = "../differ" }
24 | pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
25 |
26 | [features]
27 | extension-module = ["pyo3/extension-module"]
28 |
--------------------------------------------------------------------------------
/Dockerfile_auto_upload:
--------------------------------------------------------------------------------
1 | # https://hub.docker.com/_/debian
2 | FROM docker.io/debian:testing-slim AS build
3 | MAINTAINER Jelmer Vernooij
4 |
5 | ARG DEBIAN_FRONTEND=noninteractive
6 |
7 | RUN apt-get update --yes \
8 | && apt-get install --yes --no-install-recommends \
9 | auto-apt-proxy \
10 | iproute2 \
11 | && apt-get upgrade --yes \
12 | && apt-get satisfy --yes --no-install-recommends \
13 | ## Standard packages: ./CONTRIBUTING.md
14 | cargo \
15 | g++ \
16 | gcc \
17 | libpython3-dev \
18 | libssl-dev \
19 | pkg-config \
20 | protobuf-compiler \
21 | ## Extra packages
22 | python3-pip \
23 | && apt-get clean
24 |
25 | COPY . /code
26 |
27 | RUN pip3 install --break-system-packages --upgrade "/code[gcp,auto-upload]" \
28 | && rm -rf /code
29 |
30 | EXPOSE 9933
31 |
32 | ENTRYPOINT ["janitor-auto-upload", "--port=9933", "--listen-address=0.0.0.0"]
33 |
--------------------------------------------------------------------------------
/Dockerfile_differ:
--------------------------------------------------------------------------------
1 | # https://hub.docker.com/_/debian
2 | FROM docker.io/debian:testing-slim AS build
3 | MAINTAINER Jelmer Vernooij
4 |
5 | ARG DEBIAN_FRONTEND=noninteractive
6 |
7 | RUN apt-get update --yes \
8 | && apt-get install --yes --no-install-recommends \
9 | auto-apt-proxy \
10 | iproute2 \
11 | && apt-get upgrade --yes \
12 | && apt-get satisfy --yes --no-install-recommends \
13 | ## Standard packages: ./CONTRIBUTING.md
14 | cargo \
15 | g++ \
16 | gcc \
17 | libpython3-dev \
18 | libssl-dev \
19 | pkg-config \
20 | protobuf-compiler \
21 | ## Extra packages
22 | python3-gpg \
23 | python3-pip \
24 | && apt-get clean
25 |
26 | COPY . /code
27 |
28 | RUN pip3 install --break-system-packages --upgrade "/code[gcp,differ]" \
29 | && rm -rf /code
30 |
31 | EXPOSE 9920
32 |
33 | ENTRYPOINT ["janitor-differ", "--port=9920", "--listen-address=0.0.0.0"]
34 |
--------------------------------------------------------------------------------
/Dockerfile_publish:
--------------------------------------------------------------------------------
1 | # https://hub.docker.com/_/debian
2 | FROM docker.io/debian:testing-slim AS build
3 | MAINTAINER Jelmer Vernooij
4 |
5 | ARG DEBIAN_FRONTEND=noninteractive
6 |
7 | RUN apt-get update --yes \
8 | && apt-get install --yes --no-install-recommends \
9 | auto-apt-proxy \
10 | iproute2 \
11 | && apt-get upgrade --yes \
12 | && apt-get satisfy --yes --no-install-recommends \
13 | ## Standard packages: ./CONTRIBUTING.md
14 | cargo \
15 | g++ \
16 | gcc \
17 | libpython3-dev \
18 | libssl-dev \
19 | pkg-config \
20 | protobuf-compiler \
21 | ## Extra packages
22 | python3-gpg \
23 | python3-pip \
24 | && apt-get clean
25 |
26 | COPY . /code
27 |
28 | RUN pip3 install --break-system-packages --upgrade "/code[gcp,publish]" \
29 | && rm -rf /code
30 |
31 | EXPOSE 9912
32 |
33 | ENTRYPOINT ["janitor-publish", "--port=9912", "--listen-address=0.0.0.0"]
34 |
--------------------------------------------------------------------------------
/devnotes/branch-names.rst:
--------------------------------------------------------------------------------
1 | Goal
2 | ====
3 |
4 | For runs:
5 | * runs/$uuid/$function tags
6 | * runs/$uuid/tags/$tag tags for new/updated upstream tags
7 |
8 | There is a symref at refs/$suite/$function that points at said tag and updated
9 |
10 | Also, track this information in the database.
11 |
12 | For related repositories, create remotes:
13 |
14 | * remotes/origin/ for Debian packaging
15 | * remotes/upstream/ for Upstream
16 |
17 | Names for functions:
18 |
19 | * "main" for the main branch (packaging or otherwise)
20 | * "upstream" for the upstream import branch for Debian packages
21 | * "pristine-tar" for the pristine-tar branch
22 |
23 | Roadmap
24 | =======
25 |
26 | * Update publisher to use new tag names
27 |
28 | * Send requests to publisher to mirror origin/upstream repositories
29 | To start off with just:
30 | * name of remote ("origin", "upstream")
31 | * URL of remote
32 |
33 | * Push symrefs (refs/$suite/$function => refs/tags/$uuid/$function)
34 | + needs to be done by publisher
35 |
--------------------------------------------------------------------------------
/py/janitor/site/templates/ready-list.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block sidebar %}
3 | {% if not suite %}
4 | {% include "cupboard/sidebar.html" %}
5 | {% else %}
6 | {% include [suite + "/sidebar.html", "generic/sidebar.html"] %}
7 | {% endif %}
8 | {% endblock sidebar %}
9 | {% block page_title %}
10 | Changes Ready to Publish
11 | {% if suite %}- {{ suite }}{% endif %}
12 | {% endblock page_title %}
13 | {% block body %}
14 |
15 |
Ready to publish
16 |
17 | {% for run in runs %}
18 |
19 | {{ run.codebase }}
20 | {% set command = run.command -%}
21 | {% set result = run.result -%}
22 | {% include [run.suite + "/summary.html", "generic/summary.html"] %}
23 |
18 | Work is under way to also support Mercurial. Subversion support may
19 | also be an option, though I have yet to work out what the equivalent of
20 | pull requests in Subversion would be.
21 |
22 |
23 |
--------------------------------------------------------------------------------
/differ/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Differ crate for the Janitor project.
2 | //!
3 | //! This crate provides functionality for finding and comparing binary files.
4 |
5 | #![deny(missing_docs)]
6 |
7 | /// Module for interacting with diffoscope
8 | pub mod diffoscope;
9 |
10 | use std::ffi::{OsStr, OsString};
11 | use std::path::{Path, PathBuf};
12 |
13 | /// Find binary files in a directory.
14 | ///
15 | /// # Arguments
16 | /// * `path` - The directory to search
17 | ///
18 | /// # Returns
19 | /// An iterator of (filename, path) pairs
20 | pub fn find_binaries(path: &Path) -> impl Iterator {
21 | std::fs::read_dir(path).unwrap().filter_map(|entry| {
22 | let entry = entry.ok()?;
23 | let path = entry.path();
24 | Some((entry.file_name(), path))
25 | })
26 | }
27 |
28 | /// Check if a filename is a binary package.
29 | ///
30 | /// # Arguments
31 | /// * `name` - The filename to check
32 | ///
33 | /// # Returns
34 | /// `true` if the file is a binary package, `false` otherwise
35 | pub fn is_binary(name: &str) -> bool {
36 | name.ends_with(".deb") || name.ends_with(".udeb")
37 | }
38 |
--------------------------------------------------------------------------------
/src/logging.rs:
--------------------------------------------------------------------------------
1 | #[derive(clap::Args, Debug, Clone)]
2 | #[group()]
3 | pub struct LoggingArgs {
4 | /// Enable debug mode.
5 | #[arg(long, default_value_t = false)]
6 | pub debug: bool,
7 |
8 | /// Use Google cloud logging.
9 | #[cfg(feature = "gcp")]
10 | #[arg(long, default_value_t = false)]
11 | pub gcp_logging: bool,
12 | }
13 |
14 | impl LoggingArgs {
15 | pub fn init(&self) {
16 | #[cfg(feature = "gcp")]
17 | let gcp_logging = self.gcp_logging;
18 |
19 | #[cfg(not(feature = "gcp"))]
20 | let gcp_logging = false;
21 | init_logging(gcp_logging, self.debug);
22 | }
23 | }
24 |
25 | pub fn init_logging(gcp_logging: bool, debug_mode: bool) {
26 | #[cfg(feature = "gcp")]
27 | if gcp_logging {
28 | stackdriver_logger::init_with_cargo!("../Cargo.toml");
29 | return;
30 | }
31 |
32 | #[cfg(not(feature = "gcp"))]
33 | assert!(!gcp_logging, "GCP logging is not enabled");
34 |
35 | use log::LevelFilter;
36 | let level = if debug_mode {
37 | LevelFilter::Debug
38 | } else {
39 | LevelFilter::Info
40 | };
41 | env_logger::builder().filter(None, level).init();
42 | }
43 |
--------------------------------------------------------------------------------
/tests/test_site_simple.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Copyright (C) 2022 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | from janitor.config import read_string as read_config_string
19 | from janitor.site.simple import create_app
20 |
21 |
22 | def create_config():
23 | return read_config_string("""
24 | campaign {
25 | name: "lintian-fixes"
26 | }
27 | """)
28 |
29 |
30 | async def test_create_app():
31 | await create_app(config=create_config())
32 |
--------------------------------------------------------------------------------
/py/janitor/_publish.pyi:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | def calculate_next_try_time(finish_time: datetime, attempt_count: int) -> datetime: ...
4 | def get_merged_by_user_url(url: str, user: str) -> str | None: ...
5 | def branches_match(url_a: str | None, url_b: str | None) -> bool: ...
6 | def role_branch_url(url: str, remote_branch_name: str | None) -> str: ...
7 |
8 | class RateLimiter:
9 | def set_mps_per_bucket(self, mps_per_bucket: dict[str, dict[str, int]]) -> None: ...
10 | def check_allowed(self, bucket: str) -> None: ...
11 | def inc(self, bucket: str) -> None: ...
12 | def get_stats(self) -> dict[str, tuple[int, int | None]]: ...
13 |
14 | class SlowStartRateLimiter(RateLimiter):
15 | def __init__(self, mps_per_bucket: int | None) -> None: ...
16 |
17 | class NonRateLimiter(RateLimiter):
18 | def __init__(self) -> None: ...
19 |
20 | class FixedRateLimiter(RateLimiter):
21 | def __init__(self, mps_per_bucket: int) -> None: ...
22 |
23 | class RateLimited(Exception):
24 | def __init__(self, message: str) -> None: ...
25 |
26 | class BucketRateLimited(RateLimited):
27 | def __init__(self, bucket: str, open_mps: int, max_open_mps: int) -> None: ...
28 |
29 | bucket: str
30 | open_mps: int
31 | max_open_mps: int
32 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import importlib.resources
2 | from collections.abc import AsyncGenerator
3 |
4 | import asyncpg
5 | import pytest_asyncio
6 | import testing.postgresql
7 |
8 | from janitor.state import create_pool
9 |
10 | pytest_plugins = ["aiohttp"]
11 |
12 |
13 | @pytest_asyncio.fixture()
14 | async def db():
15 | with testing.postgresql.Postgresql() as postgresql:
16 | conn = await asyncpg.connect(postgresql.url())
17 | files = importlib.resources.files("janitor")
18 | debian_files = importlib.resources.files("janitor.debian")
19 | try:
20 | with files.joinpath("state.sql").open() as f:
21 | await conn.execute(f.read())
22 | with debian_files.joinpath("debian.sql").open() as f:
23 | await conn.execute(f.read())
24 | finally:
25 | await conn.close()
26 |
27 | db = await create_pool(postgresql.url())
28 |
29 | yield db
30 |
31 | await db.close()
32 |
33 |
34 | @pytest_asyncio.fixture()
35 | async def con(db: asyncpg.Pool) -> AsyncGenerator[asyncpg.Connection, None]:
36 | async with db.acquire() as con:
37 | yield con
38 |
39 |
40 | async def test_db_returns_janitor_db(db) -> None:
41 | assert isinstance(db, asyncpg.Pool)
42 |
--------------------------------------------------------------------------------
/worker/src/bin/debian-build.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 |
3 | #[derive(Parser)]
4 | struct Args {
5 | /// Path to configuration (JSON)
6 | #[clap(short, long)]
7 | config: Option,
8 | /// Output directory
9 | #[clap(short, long)]
10 | output_directory: std::path::PathBuf,
11 | }
12 |
13 | fn main() {
14 | let args = Args::parse();
15 |
16 | breezyshim::init();
17 |
18 | let (wt, subpath) =
19 | breezyshim::workingtree::open_containing(std::path::Path::new(".")).unwrap();
20 |
21 | let config = if let Some(config) = args.config {
22 | let config = std::fs::read_to_string(config).unwrap();
23 | serde_json::from_str(&config).unwrap()
24 | } else {
25 | serde_json::Value::Null
26 | };
27 |
28 | match janitor_worker::debian::build_from_config(
29 | &wt,
30 | &subpath,
31 | &args.output_directory,
32 | &config,
33 | &std::env::vars().collect::>(),
34 | ) {
35 | Ok(result) => serde_json::to_writer(std::io::stdout(), &result).unwrap(),
36 | Err(e) => {
37 | serde_json::to_writer(std::io::stdout(), &serde_json::to_value(e).unwrap()).unwrap();
38 | std::process::exit(1);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/publish/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "janitor-publish"
3 | version = "0.0.0"
4 | authors = ["Jelmer Vernooij "]
5 | edition.workspace = true
6 | description = "Publisher for the janitor"
7 | license = "GPL-3.0+"
8 | repository = "https://github.com/jelmer/janitor.git"
9 | homepage = "https://github.com/jelmer/janitor"
10 |
11 | [dependencies]
12 | axum = { workspace = true }
13 | breezyshim = { workspace = true, features = ["sqlx"] }
14 | chrono.workspace = true
15 | clap = { workspace = true, features = ["derive"] }
16 | debian-changelog = "0.2.0"
17 | janitor = { path = ".." }
18 | log.workspace = true
19 | minijinja = { version = "2", features = ["loader"] }
20 | pyo3.workspace = true
21 | redis = { workspace = true, features = ["tokio-comp", "json", "connection-manager"] }
22 | rslock = { workspace = true, default-features = false, features = ["tokio-comp"] }
23 | reqwest.workspace = true
24 | serde.workspace = true
25 | serde_json.workspace = true
26 | shlex.workspace = true
27 | silver-platter.workspace = true
28 | tokio = { workspace = true, features = ["full"] }
29 | url = { workspace = true, features = ["serde"] }
30 | sqlx = { workspace = true, features = ["chrono"] }
31 | maplit.workspace = true
32 | prometheus = "0.14.0"
33 |
34 | [dev-dependencies]
35 | maplit = { workspace = true }
36 |
--------------------------------------------------------------------------------
/tests/test_core.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Copyright (C) 2022 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 |
19 | from janitor import splitout_env
20 |
21 |
22 | def test_splitout_env():
23 | assert splitout_env("ls") == ({}, "ls")
24 | assert splitout_env("PATH=/bin ls") == ({"PATH": "/bin"}, "ls")
25 | assert splitout_env("PATH=/bin FOO=bar ls") == (
26 | {"PATH": "/bin", "FOO": "bar"},
27 | "ls",
28 | )
29 | assert splitout_env("PATH=/bin FOO=bar ls -l") == (
30 | {"PATH": "/bin", "FOO": "bar"},
31 | "ls -l",
32 | )
33 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Copyright (C) 2019 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | import unittest
19 |
20 | from breezy.tests import TestCaseWithTransport # noqa: F401
21 |
22 |
23 | def test_suite():
24 | names = [
25 | "archive",
26 | "artifacts",
27 | "debdiff",
28 | "debian",
29 | "runner",
30 | "site",
31 | "vcs",
32 | ]
33 | module_names = [__name__ + ".test_" + name for name in names]
34 | loader = unittest.TestLoader()
35 | return loader.loadTestsFromNames(module_names)
36 |
--------------------------------------------------------------------------------
/py/janitor/debian/debdiff.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # Copyright (C) 2019 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | from .._common import debdiff # type: ignore
19 |
20 | debdiff_is_empty = debdiff.debdiff_is_empty # type: ignore
21 | filter_boring = debdiff.filter_boring # type: ignore
22 | section_is_wdiff = debdiff.section_is_wdiff # type: ignore
23 | markdownify_debdiff = debdiff.markdownify_debdiff # type: ignore
24 | htmlize_debdiff = debdiff.htmlize_debdiff # type: ignore
25 | DebdiffError = debdiff.DebdiffError # type: ignore
26 | run_debdiff = debdiff.run_debdiff # type: ignore
27 |
--------------------------------------------------------------------------------
/worker/src/bin/generic-build.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 |
3 | #[derive(Parser)]
4 | struct Args {
5 | /// Path to configuration (JSON)
6 | #[clap(short, long)]
7 | config: Option,
8 | /// Output directory
9 | #[clap(short, long)]
10 | output_directory: std::path::PathBuf,
11 | }
12 |
13 | fn main() {
14 | let args = Args::parse();
15 |
16 | breezyshim::init();
17 |
18 | let (wt, subpath) =
19 | breezyshim::workingtree::open_containing(std::path::Path::new(".")).unwrap();
20 |
21 | let config: janitor::api::worker::GenericBuildConfig = if let Some(config) = args.config {
22 | let config = std::fs::read_to_string(config).unwrap();
23 | serde_json::from_str(&config).unwrap()
24 | } else {
25 | serde_json::from_value(serde_json::json!({})).unwrap()
26 | };
27 |
28 | match janitor_worker::generic::build_from_config(
29 | &wt,
30 | &subpath,
31 | &args.output_directory,
32 | &config,
33 | &std::env::vars().collect::>(),
34 | ) {
35 | Ok(result) => serde_json::to_writer(std::io::stdout(), &result).unwrap(),
36 | Err(e) => {
37 | serde_json::to_writer(std::io::stdout(), &serde_json::to_value(e).unwrap()).unwrap();
38 | std::process::exit(1);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/devnotes/adding-a-new-campaign.rst:
--------------------------------------------------------------------------------
1 | Adding a new campaign
2 | =====================
3 |
4 | Create a new codemod script
5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~
6 |
7 | At the core of every campaign is a script that can make changes
8 | to a version controlled branch.
9 |
10 | This script will be executed in a version controlled checkout of
11 | a source codebase, and can make changes to the codebase as it sees fit.
12 | See `this blog post `_ for more
13 | information about creating codemod scripts.
14 |
15 | You can test the script independently by running silver-platter, e.g.
16 |
17 | ``./debian-svp apply --command=myscript --dry-run --diff`` (from a checkout)
18 | or
19 |
20 | ``./debian-svp run --command=myscript --dry-run --diff package-name``
21 |
22 | Add configuration for the campaign
23 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24 |
25 | In janitor.conf, add a section for the campaign. E.g.::
26 |
27 | campaign {
28 | name: "some-name"
29 | branch_name: "some-name"
30 | debian_build {
31 | build_suffix: "suf"
32 | }
33 | }
34 |
35 | Add script for finding candidates
36 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
37 |
38 | Add a script that can gather candidates for the new campaign. This script should
39 | be run regularly to find new candidates to schedule, with its JSON output
40 | uploaded to $RUNNER_URL/candidates.
41 |
--------------------------------------------------------------------------------
/py/janitor/site/templates/result-codes/invalid-upstream-version-format.html:
--------------------------------------------------------------------------------
1 |
2 | The upstream version that was picked contains characters that
3 | are invalid in Debian version strings.
4 |
5 |
6 | For version strings that come from upstream tags, this can
7 | be because the upstream tags use characters that are not valid
8 | in Debian version strings. The Janitor currently only applies
9 | very basic version mangling to upstream tags:
10 |
11 |
12 | Strip release- prefixes and -release suffixes
13 |
14 |
15 | Strip package- prefixes
16 |
17 |
18 | Strip v prefixes
19 |
20 |
21 | Replace any underscores with dots if there are no other
22 | dots in the version. This is done for compatibility with CVS style tags,
23 | which usually did not use dots.
24 |
25 |
26 |
27 | For version strings that come from uscan, no additional mangling
28 | is performed besides the mangling that uscan already does.
29 |
30 |
31 | In some cases, the version string matching is overly broad - and the
32 | lintian-brush could possibly replace the first group with @ANY_VERSION@ to
33 | fix the watch file.
34 |
33 | To install the packages from this changeset, use the following
34 | sources configuration (with archive keyring stored in
35 | /etc/apt/keyrings/debian-janitor.gpg):
36 |
37 |
38 |
39 |
40 |
41 | deb "[arch=amd64 signed-by=/etc/apt/keyrings/debian-janitor.gpg]" {{ url.join(URL('/')) }} cs/{{ changeset.id }} main
42 | deb-src "[arch=amd64 signed-by=/etc/apt/keyrings/debian-janitor.gpg]" {{ url.join(URL('/')) }} cs/{{ changeset.id }} main
43 |
9 | The initial version of the Janitor was written by Jelmer Vernooij.
10 |
11 |
12 | The code for the Debian-specific instance has now been split out into a separate
13 | repository.
14 |
15 |
Many thanks to everybody who has provided feedback and helped make the Janitor better.
16 |
Source Code
17 |
The Janitor itself is mostly written in Python, using the following libraries and services:
18 |
19 |
20 | The style for the website is based on Alabaster 0.7.8 theme for sphinx.
21 |
22 |
23 | Breezy
24 | provides abstractions over the version control system (Git, Bazaar,
25 | Mercurial, Subversion) and the supported hosting platforms (GitHub,
26 | GitLab, Launchpad).
27 |
28 |
29 | Silver-Platter
31 | ties this all together; it manages branches, invokes codemods and pushes back or creates
32 | merge proposals.
33 |
43 | {% endblock body %}
44 |
--------------------------------------------------------------------------------
/py/janitor/_launchpad.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # Copyright (C) 2019 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 |
19 | def override_launchpad_consumer_name():
20 | from breezy.forge import ForgeLoginRequired
21 | from breezy.plugins.launchpad import lp_api
22 | from launchpadlib.credentials import RequestTokenAuthorizationEngine
23 | from launchpadlib.launchpad import Launchpad
24 |
25 | class LoginRequiredAuthorizationEngine(RequestTokenAuthorizationEngine):
26 | def make_end_user_authorize_token(self, credentials, request_token):
27 | raise ForgeLoginRequired(self.web_root)
28 |
29 | def connect_launchpad(
30 | base_url, timeout=None, proxy_info=None, version=Launchpad.DEFAULT_VERSION
31 | ):
32 | cache_directory = lp_api.get_cache_directory()
33 | credential_store = lp_api.BreezyCredentialStore()
34 | authorization_engine = LoginRequiredAuthorizationEngine(
35 | base_url, consumer_name="Janitor"
36 | )
37 | return Launchpad.login_with(
38 | "Janitor",
39 | base_url,
40 | cache_directory,
41 | timeout=timeout,
42 | credential_store=credential_store,
43 | authorization_engine=authorization_engine,
44 | version=version,
45 | )
46 |
47 | lp_api.connect_launchpad = connect_launchpad
48 |
--------------------------------------------------------------------------------
/py/janitor/site/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Janitor -
6 | {% block page_title %}
7 | {% endblock page_title %}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% include "analytics.html" ignore missing without context %}
26 |
53 | {% endblock body %}
54 |
--------------------------------------------------------------------------------
/tests/test_artifacts.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Copyright (C) 2021 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | import os
19 | import shutil
20 | import tempfile
21 |
22 | import pytest
23 |
24 | from janitor.artifacts import ArtifactsMissing, LocalArtifactManager
25 |
26 |
27 | @pytest.fixture
28 | def local_artifact_manager():
29 | path = tempfile.mkdtemp()
30 | try:
31 | yield LocalArtifactManager(path)
32 | finally:
33 | shutil.rmtree(path)
34 |
35 |
36 | @pytest.mark.asyncio
37 | async def test_store_twice(local_artifact_manager):
38 | manager = local_artifact_manager
39 | with tempfile.TemporaryDirectory() as td:
40 | with open(os.path.join(td, "somefile"), "w") as f:
41 | f.write("lalala")
42 | await manager.store_artifacts("some-run-id", td)
43 | await manager.store_artifacts("some-run-id", td)
44 |
45 |
46 | @pytest.mark.asyncio
47 | async def test_store_and_retrieve(local_artifact_manager):
48 | manager = local_artifact_manager
49 | with tempfile.TemporaryDirectory() as td:
50 | with open(os.path.join(td, "somefile"), "w") as f:
51 | f.write("lalala")
52 | await manager.store_artifacts("some-run-id", td)
53 | with tempfile.TemporaryDirectory() as td:
54 | await manager.retrieve_artifacts("some-run-id", td)
55 | assert ["somefile"] == os.listdir(td)
56 |
57 |
58 | @pytest.mark.asyncio
59 | async def test_retrieve_nonexistent(local_artifact_manager):
60 | manager = local_artifact_manager
61 | with tempfile.TemporaryDirectory() as td:
62 | with pytest.raises(ArtifactsMissing):
63 | await manager.retrieve_artifacts("some-run-id", td)
64 |
--------------------------------------------------------------------------------
/py/janitor/review.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # Copyright (C) 2018 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | from typing import Optional
19 |
20 | from yarl import URL
21 |
22 | from .schedule import do_schedule
23 |
24 |
25 | async def store_review(
26 | conn,
27 | session,
28 | runner_url,
29 | run_id: str,
30 | verdict: str,
31 | comment: Optional[str],
32 | reviewer: Optional[str],
33 | is_qa_reviewer: bool,
34 | ):
35 | async with conn.transaction():
36 | if verdict == "reschedule":
37 | verdict = "rejected"
38 |
39 | run = await conn.fetchrow(
40 | "SELECT suite, codebase FROM run WHERE id = $1", run_id
41 | )
42 | await do_schedule(
43 | conn,
44 | campaign=run["suite"],
45 | refresh=True,
46 | requester=f"reviewer ({reviewer})",
47 | bucket="default",
48 | codebase=run["codebase"],
49 | )
50 |
51 | if verdict != "abstained" and is_qa_reviewer:
52 | async with session.post(
53 | URL(runner_url) / "runs" / run_id,
54 | json={"publish_status": verdict},
55 | raise_for_status=True,
56 | ):
57 | pass
58 | await conn.execute(
59 | "INSERT INTO review (run_id, comment, reviewer, verdict) VALUES "
60 | " ($1, $2, $3, $4) ON CONFLICT (run_id, reviewer) "
61 | "DO UPDATE SET verdict = EXCLUDED.verdict, comment = EXCLUDED.comment, "
62 | "reviewed_at = NOW()",
63 | run_id,
64 | comment,
65 | reviewer,
66 | verdict,
67 | )
68 |
--------------------------------------------------------------------------------
/tests/test_archive.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Copyright (C) 2022 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 |
19 | import hashlib
20 | import os
21 | from tempfile import TemporaryDirectory
22 |
23 | from debian.deb822 import Release
24 |
25 | from janitor.config import read_string as read_config_string
26 | from janitor.debian.archive import HashedFileWriter, create_app
27 |
28 |
29 | async def create_client(aiohttp_client, config=None):
30 | if config is None:
31 | config = read_config_string("")
32 | return await aiohttp_client(
33 | await create_app(None, config, "/tmp", None, gpg_context=None)
34 | )
35 |
36 |
37 | async def test_health(aiohttp_client):
38 | client = await create_client(aiohttp_client)
39 |
40 | resp = await client.get("/health")
41 | assert resp.status == 200
42 | text = await resp.text()
43 | assert text == "ok"
44 |
45 |
46 | async def test_ready(aiohttp_client):
47 | client = await create_client(aiohttp_client)
48 |
49 | resp = await client.get("/ready")
50 | assert resp.status == 200
51 | text = await resp.text()
52 | assert text == ""
53 |
54 |
55 | def test_hash_file_writer():
56 | with TemporaryDirectory() as td:
57 | r = Release()
58 | with HashedFileWriter(r, td, "foo/bar") as w:
59 | w.write(b"chunk1")
60 | w.write(b"chunk2")
61 | w.done()
62 | md5hex = hashlib.md5(b"chunk1chunk2").hexdigest()
63 | with open(os.path.join(td, "foo", "by-hash", "MD5Sum", md5hex), "rb") as f:
64 | assert f.read() == b"chunk1chunk2"
65 | with open(os.path.join(td, "foo", "bar"), "rb") as f:
66 | assert f.read() == b"chunk1chunk2"
67 | assert r["MD5Sum"] == [{"md5sum": md5hex, "name": "foo/bar", "size": 12}]
68 |
--------------------------------------------------------------------------------
/tests/test_logs.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Copyright (C) 2022 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | import os
19 | import tempfile
20 | from datetime import datetime
21 |
22 | import pytest
23 |
24 | from janitor.logs import FileSystemLogFileManager, GCSLogFileManager, S3LogFileManager
25 |
26 |
27 | def test_s3_log_file_manager():
28 | S3LogFileManager("https://some-url/")
29 |
30 |
31 | def test_gcs_log_file_manager():
32 | GCSLogFileManager("gs://foo/")
33 |
34 |
35 | async def test_file_log_file_manager():
36 | with tempfile.TemporaryDirectory() as td:
37 | async with FileSystemLogFileManager(td) as lm:
38 | assert not await lm.has_log("mypkg", "run-id", "foo.log")
39 | with pytest.raises(FileNotFoundError):
40 | await lm.get_log("mypkg", "run-id", "foo.log")
41 | with pytest.raises(FileNotFoundError):
42 | await lm.get_ctime("mypkg", "run-id", "foo.log")
43 | with pytest.raises(FileNotFoundError):
44 | await lm.delete_log("mypkg", "run-id", "foo.log")
45 | with tempfile.NamedTemporaryFile(suffix=".log") as f:
46 | f.write(b"foo bar\n")
47 | f.flush()
48 | await lm.import_log("mypkg", "run-id", f.name)
49 | logname = os.path.basename(f.name)
50 | assert await lm.has_log("mypkg", "run-id", logname)
51 | assert (await lm.get_log("mypkg", "run-id", logname)).read() == b"foo bar\n"
52 | assert isinstance(await lm.get_ctime("mypkg", "run-id", logname), datetime)
53 | assert [x async for x in lm.iter_logs()] == [("mypkg", "run-id", [logname])]
54 | await lm.delete_log("mypkg", "run-id", logname)
55 | assert not await lm.has_log("mypkg", "run-id", logname)
56 |
--------------------------------------------------------------------------------
/differ-py/src/lib.rs:
--------------------------------------------------------------------------------
1 | use pyo3::exceptions::{PyRuntimeError, PyTimeoutError, PyValueError};
2 | use pyo3::prelude::*;
3 |
4 | #[pyfunction]
5 | #[pyo3(signature = (old_binaries, new_binaries, timeout = None, memory_limit = None, diffoscope_command = None))]
6 | fn run_diffoscope<'a>(
7 | py: Python<'a>,
8 | old_binaries: Vec<(String, String)>,
9 | new_binaries: Vec<(String, String)>,
10 | timeout: Option,
11 | memory_limit: Option,
12 | diffoscope_command: Option,
13 | ) -> PyResult> {
14 | pyo3_async_runtimes::tokio::future_into_py(py, async move {
15 | let old_binaries = old_binaries
16 | .iter()
17 | .map(|(path, hash)| (path.as_str(), hash.as_str()))
18 | .collect::>();
19 | let new_binaries = new_binaries
20 | .iter()
21 | .map(|(path, hash)| (path.as_str(), hash.as_str()))
22 | .collect::>();
23 |
24 | let o = janitor_differ::diffoscope::run_diffoscope(
25 | old_binaries.as_slice(),
26 | new_binaries.as_slice(),
27 | timeout,
28 | memory_limit,
29 | diffoscope_command.as_deref(),
30 | )
31 | .await
32 | .map_err(|e| match e {
33 | janitor_differ::diffoscope::DiffoscopeError::Timeout => {
34 | PyTimeoutError::new_err("Diffoscope timed out")
35 | }
36 | janitor_differ::diffoscope::DiffoscopeError::Io(e) => e.into(),
37 | janitor_differ::diffoscope::DiffoscopeError::Other(e) => PyRuntimeError::new_err(e),
38 | janitor_differ::diffoscope::DiffoscopeError::Serde(e) => {
39 | PyValueError::new_err(e.to_string())
40 | }
41 | })?;
42 | Ok(Python::with_gil(|py| o.to_object(py)))
43 | })
44 | }
45 |
46 | #[pyfunction]
47 | fn filter_boring_udiff(
48 | udiff: &str,
49 | old_version: &str,
50 | new_version: &str,
51 | display_version: &str,
52 | ) -> PyResult {
53 | let o = janitor_differ::diffoscope::filter_boring_udiff(
54 | udiff,
55 | old_version,
56 | new_version,
57 | display_version,
58 | )
59 | .map_err(|e| PyValueError::new_err(e.to_string()))?;
60 | Ok(o)
61 | }
62 |
63 | #[pymodule]
64 | pub fn _differ(m: &Bound) -> PyResult<()> {
65 | pyo3_log::init();
66 |
67 | m.add_function(wrap_pyfunction!(run_diffoscope, m)?)?;
68 | m.add_function(wrap_pyfunction!(filter_boring_udiff, m)?)?;
69 |
70 | Ok(())
71 | }
72 |
--------------------------------------------------------------------------------
/docs/flow.md:
--------------------------------------------------------------------------------
1 | Package metadata
2 | ================
3 |
4 | Package metadata contains mostly static information about a package, imported
5 | straight from the archive. This includes the package name, maintainer email,
6 | uploader emails (if any) as well as the version control information
7 | (vcs type, URL, subpath) and optionally popularity.
8 |
9 | The "schedule" job regularly imports package metadata. On Debian, this information
10 | comes from UDD. On other Debian-like distributions, it's imported from the
11 | apt sources file.
12 |
13 | The importing has two components:
14 |
15 | * A script that can output Package() protobufs (see janitor/package_metadata.proto) to standard out
16 | * An importer that reads these protobufs on standard in and updates the database (janitor.package_metadata)
17 |
18 | Candidates
19 | ==========
20 |
21 | Once the janitor knows about a package, candidates can be created. A candidate
22 | is a bit of data that a particular suite (TODO: better name) (e.g. lintian-fixes)
23 | can be run on a particular package and that there is some chance it will yield
24 | changes.
25 |
26 | Candidates include information like:
27 |
28 | * value: a relative number that explains how useful this change would be
29 | * success_chance: an estimate of how likely this change is to succeed and result in a build
30 | * context: some indicator of the current state of the world. Used to avoid retrying
31 | builds if nothing has really changed. e.g. for new upstream releases, this
32 | is the upstream version number of the latest release
33 |
34 | Like package metadata, candidates are generated by a script that writes
35 | YAML to standard output. Candidate generation scripts
36 | can be really complicated - allowing for more optimal scheduling - or really
37 | simple, in which case they just output a candidate for each package in a suite
38 | with fixed settings for value and succes_chance.
39 |
40 | Scheduling
41 | ==========
42 |
43 | Once candidates have been created, the schedule job (``janitor.schedule``)
44 | inserts new entries into the queue, taking into account a variety of factors:
45 |
46 | * success chance
47 | * value
48 | * popularity of the package if known (from popcon)
49 | * previous success rate (for the suite/package combination and the package itself)
50 | * previous run duration
51 | * whether the context has changed since the last run
52 |
53 | The queue consists of prioritized buckets. Manually requested runs, runs triggered
54 | by the publisher (e.g. to resolve merge conflicts) and retried runs are always
55 | executed before runs that were scheduled by the scheduler.
56 |
--------------------------------------------------------------------------------
/tests/test_debdiff.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Copyright (C) 2019 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | from janitor.debian.debdiff import filter_boring
19 |
20 |
21 | def test_just_versions():
22 | debdiff = """\
23 | File lists identical (after any substitutions)
24 |
25 | Control files of package acpi-fakekey: lines which differ (wdiff format)
26 | ------------------------------------------------------------------------
27 | Version: [-0.143-4~jan+unchanged1-] {+0.143-5~jan+lint1+}
28 |
29 | Control files of package acpi-fakekey-dbgsym: lines which differ (wdiff format)
30 | -------------------------------------------------------------------------------
31 | Depends: acpi-fakekey (= [-0.143-4~jan+unchanged1)-] {+0.143-5~jan+lint1)+}
32 | Version: [-0.143-4~jan+unchanged1-] {+0.143-5~jan+lint1+}
33 |
34 | Control files of package acpi-support: lines which differ (wdiff format)
35 | ------------------------------------------------------------------------
36 | Version: [-0.143-4~jan+unchanged1-] {+0.143-5~jan+lint1+}
37 |
38 | Control files of package acpi-support-base: lines which differ (wdiff format)
39 | -----------------------------------------------------------------------------
40 | Version: [-0.143-4~jan+unchanged1-] {+0.143-5~jan+lint1+}
41 | """
42 | newdebdiff = filter_boring(debdiff, "0.143-4~jan+unchanged1", "0.143-5~jan+lint1")
43 | assert (
44 | newdebdiff
45 | == """\
46 | File lists identical (after any substitutions)
47 |
48 | No differences were encountered between the control files of package \
49 | acpi-fakekey
50 |
51 | No differences were encountered between the control files of package \
52 | acpi-fakekey-dbgsym
53 |
54 | No differences were encountered between the control files of package \
55 | acpi-support
56 |
57 | No differences were encountered between the control files of package \
58 | acpi-support-base
59 | """
60 | )
61 |
--------------------------------------------------------------------------------
/py/janitor/site/templates/cupboard/done-list.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block sidebar %}
3 | {% include "cupboard/sidebar.html" %}
4 | {% endblock sidebar %}
5 | {% block page_title %}
6 | Cupboard - Changes That Have Been Merged or Pushed
7 | {% endblock page_title %}
8 | {% block body %}
9 |
55 | {% endblock body %}
56 |
--------------------------------------------------------------------------------
/tests/test_site_macros.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Copyright (C) 2022 Jelmer Vernooij
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation; either version 2 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program; if not, write to the Free Software
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 |
18 | from jinja2 import Environment, select_autoescape
19 |
20 | from janitor.site import template_loader
21 |
22 | env = Environment(loader=template_loader, autoescape=select_autoescape(["html", "xml"]))
23 |
24 |
25 | def test_display_branch_url():
26 | template = env.get_template("run_util.html")
27 | assert (
28 | str(
29 | template.module.display_branch_url( # type: ignore
30 | None, "https://github.com/jelmer/example.git"
31 | )
32 | )
33 | == """\
34 |
35 |
36 | https://github.com/jelmer/example.git
37 |
38 | """
39 | )
40 | assert (
41 | str(
42 | template.module.display_branch_url( # type: ignore
43 | "https://github.com/jelmer/example.git",
44 | "https://github.com/jelmer/example",
45 | )
46 | )
47 | == """\
48 |
49 |
50 | https://github.com/jelmer/example
51 |
52 | """
53 | )
54 |
55 |
56 | def test_display_publish_blockers():
57 | template = env.get_template("run_util.html")
58 | assert (
59 | str(
60 | template.module.display_publish_blockers( # type: ignore
61 | {}
62 | )
63 | )
64 | == """\
65 |
66 |