Merge branch 'master' into persona

Conflicts:
	src/lib/Hydra/Helper/CatalystUtils.pm
	src/root/layout.tt
	src/root/topbar.tt
	src/root/user.tt
This commit is contained in:
Eelco Dolstra 2013-11-05 11:11:48 +01:00
commit b54cfbf032
114 changed files with 3593 additions and 1883 deletions

View file

@ -1,4 +1,8 @@
SUBDIRS = src tests doc SUBDIRS = src tests doc
BOOTCLEAN_SUBDIRS = $(SUBDIRS) BOOTCLEAN_SUBDIRS = $(SUBDIRS)
DIST_SUBDIRS = $(SUBDIRS) DIST_SUBDIRS = $(SUBDIRS)
EXTRA_DIST = hydra-module.nix
install-data-local: hydra-module.nix
$(INSTALL) -d $(DESTDIR)$(datadir)/nix
$(INSTALL_DATA) hydra-module.nix $(DESTDIR)$(datadir)/nix/

View file

@ -52,8 +52,7 @@ then
NIX_STATE_DIR="$TMPDIR" NIX_STATE_DIR="$TMPDIR"
export NIX_STATE_DIR export NIX_STATE_DIR
fi fi
if "$NIX_STORE_PROGRAM" --timeout 123 -q > /dev/null 2>&1 if NIX_REMOTE=daemon "$NIX_STORE_PROGRAM" --timeout 123 -q; then
then
AC_MSG_RESULT([yes]) AC_MSG_RESULT([yes])
else else
AC_MSG_RESULT([no]) AC_MSG_RESULT([no])
@ -68,7 +67,7 @@ LDFLAGS="$LDFLAGS -L$nix/lib/nix"
AC_CHECK_HEADER([store-api.hh], [:], AC_CHECK_HEADER([store-api.hh], [:],
[AC_MSG_ERROR([Nix headers not found; please install Nix or check the `--with-nix' option.])]) [AC_MSG_ERROR([Nix headers not found; please install Nix or check the `--with-nix' option.])])
AC_CHECK_LIB([expr], [_ZN3nix9EvalState17parseExprFromFileESs], [:], AC_CHECK_LIB([expr], [_ZN3nix9EvalState8evalFileERKSsRNS_5ValueE], [:],
[AC_MSG_ERROR([Nix library not found; please install Nix or check the `--with-nix' option.])]) [AC_MSG_ERROR([Nix library not found; please install Nix or check the `--with-nix' option.])])
CPPFLAGS="$old_CPPFLAGS" CPPFLAGS="$old_CPPFLAGS"

7
dev-shell Executable file
View file

@ -0,0 +1,7 @@
#! /bin/sh
s=$(type -p nix-shell)
exec $s release.nix -A build.x86_64-linux --exclude tarball --command "
export NIX_REMOTE=daemon
export NIX_PATH='$NIX_PATH'
export NIX_BUILD_SHELL=$(type -p bash)
exec $s release.nix -A tarball" "$@"

View file

@ -127,3 +127,13 @@
succeed in the nixpkgs:trunk jobset: succeed in the nixpkgs:trunk jobset:
select job, system from builds b natural join buildresultinfo where project = 'nixpkgs' and jobset = 'stdenv' and iscurrent = 1 and finished = 1 and buildstatus != 0 and exists (select 1 from builds natural join buildresultinfo where project = 'nixpkgs' and jobset = 'trunk' and job = b.job and system = b.system and iscurrent = 1 and finished = 1 and buildstatus = 0) order by job, system; select job, system from builds b natural join buildresultinfo where project = 'nixpkgs' and jobset = 'stdenv' and iscurrent = 1 and finished = 1 and buildstatus != 0 and exists (select 1 from builds natural join buildresultinfo where project = 'nixpkgs' and jobset = 'trunk' and job = b.job and system = b.system and iscurrent = 1 and finished = 1 and buildstatus = 0) order by job, system;
* Get all Nixpkgs jobs that have never built succesfully:
select project, jobset, job from builds b1
where project = 'nixpkgs' and jobset = 'trunk' and iscurrent = 1
group by project, jobset, job
having not exists
(select 1 from builds b2 where b1.project = b2.project and b1.jobset = b2.jobset and b1.job = b2.job and finished = 1 and buildstatus = 0)
order by project, jobset, job;

View file

@ -1,4 +1,4 @@
DOCBOOK_FILES = installation.xml introduction.xml manual.xml projects.xml DOCBOOK_FILES = installation.xml introduction.xml manual.xml projects.xml hacking.xml
EXTRA_DIST = $(DOCBOOK_FILES) EXTRA_DIST = $(DOCBOOK_FILES)

39
doc/manual/hacking.xml Normal file
View file

@ -0,0 +1,39 @@
<appendix xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:id="chap-hacking">
<title>Hacking</title>
<para>This section provides some notes on how to hack on Hydra. To
get the latest version of Hydra from GitHub:
<screen>
$ git clone git://github.com/NixOS/hydra.git
$ cd hydra
</screen>
</para>
<para>To build it and its dependencies:
<screen>
$ nix-build release.nix -A build.x86_64-linux
</screen>
</para>
<para>To build all dependencies and start a shell in which all
environment variables (such as <envar>PERL5LIB</envar>) are set up so
that those dependencies can be found:
<screen>
$ ./dev-shell
</screen>
To build Hydra, you should then do:
<screen>
[nix-shell]$ ./bootstrap
[nix-shell]$ configurePhase
[nix-shell]$ make
</screen>
You can run the Hydra web server in your source tree as follows:
<screen>
$ ./src/script/hydra-server
</screen>
</para>
</appendix>

View file

@ -52,8 +52,7 @@
<copyright> <copyright>
<year>2009</year> <year>2009-2013</year>
<year>2010</year>
<holder>Eelco Dolstra</holder> <holder>Eelco Dolstra</holder>
</copyright> </copyright>
@ -64,6 +63,7 @@
<xi:include href="introduction.xml" /> <xi:include href="introduction.xml" />
<xi:include href="installation.xml" /> <xi:include href="installation.xml" />
<xi:include href="projects.xml" /> <xi:include href="projects.xml" />
<xi:include href="hacking.xml" />
</book> </book>

View file

@ -7,13 +7,7 @@ let
baseDir = "/var/lib/hydra"; baseDir = "/var/lib/hydra";
hydraConf = pkgs.writeScript "hydra.conf" hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig;
''
using_frontend_proxy 1
base_uri ${cfg.hydraURL}
notification_sender ${cfg.notificationSender}
max_servers 25
'';
env = env =
{ NIX_REMOTE = "daemon"; { NIX_REMOTE = "daemon";
@ -28,7 +22,7 @@ let
serverEnv = env // serverEnv = env //
{ HYDRA_LOGO = if cfg.logo != null then cfg.logo else ""; { HYDRA_LOGO = if cfg.logo != null then cfg.logo else "";
HYDRA_TRACKER = cfg.tracker; HYDRA_TRACKER = cfg.tracker;
}; } // (optionalAttrs cfg.debugServer { DBIC_TRACE = 1; });
in in
{ {
@ -64,6 +58,15 @@ in
''; '';
}; };
listenHost = mkOption {
default = "*";
example = "localhost";
description = ''
The hostname or address to listen on or <literal>*</literal> to listen
on all interfaces.
'';
};
port = mkOption { port = mkOption {
default = 3000; default = 3000;
description = '' description = ''
@ -112,6 +115,17 @@ in
''; '';
}; };
debugServer = mkOption {
default = false;
type = types.bool;
description = "Whether to run the server in debug mode";
};
extraConfig = mkOption {
type = types.lines;
description = "Extra lines for the hydra config";
};
}; };
}; };
@ -120,6 +134,14 @@ in
###### implementation ###### implementation
config = mkIf cfg.enable { config = mkIf cfg.enable {
services.hydra.extraConfig =
''
using_frontend_proxy 1
base_uri ${cfg.hydraURL}
notification_sender ${cfg.notificationSender}
max_servers 25
'';
environment.systemPackages = [ cfg.hydra ]; environment.systemPackages = [ cfg.hydra ];
users.extraUsers.hydra = users.extraUsers.hydra =
@ -151,14 +173,36 @@ in
systemd.services."hydra-init" = systemd.services."hydra-init" =
{ wantedBy = [ "multi-user.target" ]; { wantedBy = [ "multi-user.target" ];
requires = [ "postgresql.service" ];
after = [ "postgresql.service" ];
environment = env; environment = env;
script = '' script = ''
mkdir -p ${baseDir}/data mkdir -p ${baseDir}/data
chown hydra ${baseDir}/data chown hydra ${baseDir}/data
ln -sf ${hydraConf} ${baseDir}/data/hydra.conf ln -sf ${hydraConf} ${baseDir}/data/hydra.conf
pass=$(HOME=/root ${pkgs.openssl}/bin/openssl rand -base64 32)
if [ ! -f ${baseDir}/.pgpass ]; then
${config.services.postgresql.package}/bin/psql postgres << EOF
CREATE USER hydra PASSWORD '$pass';
EOF
${config.services.postgresql.package}/bin/createdb -O hydra hydra
cat > ${baseDir}/.pgpass-tmp << EOF
localhost:*:hydra:hydra:$pass
EOF
chown hydra ${baseDir}/.pgpass-tmp
chmod 600 ${baseDir}/.pgpass-tmp
mv ${baseDir}/.pgpass-tmp ${baseDir}/.pgpass
fi
${pkgs.shadow}/bin/su hydra -c ${cfg.hydra}/bin/hydra-init ${pkgs.shadow}/bin/su hydra -c ${cfg.hydra}/bin/hydra-init
${config.services.postgresql.package}/bin/psql hydra << EOF
BEGIN;
INSERT INTO Users(userName, emailAddress, password) VALUES ('admin', '${cfg.notificationSender}', '$(echo -n $pass | sha1sum | cut -c1-40)');
INSERT INTO UserRoles(userName, role) values('admin', 'admin');
COMMIT;
EOF
''; '';
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
}; };
systemd.services."hydra-server" = systemd.services."hydra-server" =
@ -167,7 +211,7 @@ in
after = [ "hydra-init.service" ]; after = [ "hydra-init.service" ];
environment = serverEnv; environment = serverEnv;
serviceConfig = serviceConfig =
{ ExecStart = "@${cfg.hydra}/bin/hydra-server hydra-server -f -h \* --max_spare_servers 5 --max_servers 25 --max_requests 100"; { ExecStart = "@${cfg.hydra}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' --max_spare_servers 5 --max_servers 25 --max_requests 100${optionalString cfg.debugServer " -d"}";
User = "hydra"; User = "hydra";
Restart = "always"; Restart = "always";
}; };
@ -177,7 +221,7 @@ in
{ wantedBy = [ "multi-user.target" ]; { wantedBy = [ "multi-user.target" ];
wants = [ "hydra-init.service" ]; wants = [ "hydra-init.service" ];
after = [ "hydra-init.service" "network.target" ]; after = [ "hydra-init.service" "network.target" ];
path = [ pkgs.nettools pkgs.ssmtp ]; path = [ pkgs.nettools ];
environment = env; environment = env;
serviceConfig = serviceConfig =
{ ExecStartPre = "${cfg.hydra}/bin/hydra-queue-runner --unlock"; { ExecStartPre = "${cfg.hydra}/bin/hydra-queue-runner --unlock";
@ -191,7 +235,7 @@ in
{ wantedBy = [ "multi-user.target" ]; { wantedBy = [ "multi-user.target" ];
wants = [ "hydra-init.service" ]; wants = [ "hydra-init.service" ];
after = [ "hydra-init.service" "network.target" ]; after = [ "hydra-init.service" "network.target" ];
path = [ pkgs.nettools pkgs.ssmtp ]; path = [ pkgs.nettools ];
environment = env; environment = env;
serviceConfig = serviceConfig =
{ ExecStart = "@${cfg.hydra}/bin/hydra-evaluator hydra-evaluator"; { ExecStart = "@${cfg.hydra}/bin/hydra-evaluator hydra-evaluator";
@ -227,8 +271,9 @@ in
compressLogs = pkgs.writeScript "compress-logs" '' compressLogs = pkgs.writeScript "compress-logs" ''
#! ${pkgs.stdenv.shell} -e #! ${pkgs.stdenv.shell} -e
touch -d 'last month' r find /nix/var/log/nix/drvs \
find /nix/var/log/nix/drvs -type f -a ! -newer r -name '*.drv' | xargs bzip2 -v -type f -a ! -newermt 'last month' \
-name '*.drv' -exec bzip2 -v {} +
''; '';
in in
[ "*/5 * * * * root ${checkSpace} &> ${baseDir}/data/checkspace.log" [ "*/5 * * * * root ${checkSpace} &> ${baseDir}/data/checkspace.log"

View file

@ -6,7 +6,7 @@ let
pkgs = import <nixpkgs> {}; pkgs = import <nixpkgs> {};
genAttrs' = pkgs.lib.genAttrs [ "x86_64-linux" "i686-linux" ]; genAttrs' = pkgs.lib.genAttrs [ "x86_64-linux" ];
in rec { in rec {
@ -24,13 +24,17 @@ in rec {
versionSuffix = if officialRelease then "" else "pre${toString hydraSrc.revCount}-${hydraSrc.gitTag}"; versionSuffix = if officialRelease then "" else "pre${toString hydraSrc.revCount}-${hydraSrc.gitTag}";
preConfigure = '' preHook = ''
# TeX needs a writable font cache. # TeX needs a writable font cache.
export VARTEXFONTS=$TMPDIR/texfonts export VARTEXFONTS=$TMPDIR/texfonts
addToSearchPath PATH $(pwd)/src/script
addToSearchPath PATH $(pwd)/src/c
addToSearchPath PERL5LIB $(pwd)/src/lib
''; '';
configureFlags = configureFlags =
[ "--with-nix=${nix}" [ "--with-nix=${nixUnstable}"
"--with-docbook-xsl=${docbook_xsl}/xml/xsl/docbook" "--with-docbook-xsl=${docbook_xsl}/xml/xsl/docbook"
]; ];
@ -88,6 +92,7 @@ in rec {
PadWalker PadWalker
CatalystDevel CatalystDevel
Readonly Readonly
SetScalar
SQLSplitStatement SQLSplitStatement
Starman Starman
SysHostnameLong SysHostnameLong
@ -95,6 +100,7 @@ in rec {
TextDiff TextDiff
TextTable TextTable
XMLSimple XMLSimple
NetAmazonS3
nix git nix git
]; ];
}; };
@ -108,17 +114,20 @@ in rec {
buildInputs = buildInputs =
[ makeWrapper libtool unzip nukeReferences pkgconfig boehmgc sqlite [ makeWrapper libtool unzip nukeReferences pkgconfig boehmgc sqlite
gitAndTools.topGit mercurial subversion bazaar openssl bzip2 gitAndTools.topGit mercurial darcs subversion bazaar openssl bzip2
guile # optional, for Guile + Guix support guile # optional, for Guile + Guix support
perlDeps perl perlDeps perl
]; ];
hydraPath = lib.makeSearchPath "bin" ( hydraPath = lib.makeSearchPath "bin" (
[ libxslt sqlite subversion openssh nix coreutils findutils [ libxslt sqlite subversion openssh nix coreutils findutils
gzip bzip2 lzma gnutar unzip git gitAndTools.topGit mercurial gnused graphviz bazaar gzip bzip2 lzma gnutar unzip git gitAndTools.topGit mercurial darcs gnused graphviz bazaar
] ++ lib.optionals stdenv.isLinux [ rpm dpkg cdrkit ] ); ] ++ lib.optionals stdenv.isLinux [ rpm dpkg cdrkit ] );
preConfigure = "patchShebangs ."; preCheck = ''
patchShebangs .
export LOGNAME=${LOGNAME:-foo}
'';
postInstall = '' postInstall = ''
mkdir -p $out/nix-support mkdir -p $out/nix-support
@ -134,14 +143,13 @@ in rec {
done done
''; # */ ''; # */
LOGNAME = "foo";
meta.description = "Build of Hydra on ${system}"; meta.description = "Build of Hydra on ${system}";
passthru.perlDeps = perlDeps;
}); });
tests.install = genAttrs' (system: tests.install = genAttrs' (system:
with import <nixos/lib/testing.nix> { inherit system; }; with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
let hydra = builtins.getAttr system build; in # build.${system} let hydra = builtins.getAttr system build; in # build.${system}
simpleTest { simpleTest {
machine = machine =
@ -169,8 +177,8 @@ in rec {
}); });
tests.api = genAttrs' (system: tests.api = genAttrs' (system:
with import <nixos/lib/testing.nix> { inherit system; }; with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
let hydra = builtins.getAttr system build; in # build.${system} let hydra = builtins.getAttr system build; in # build."${system}"
simpleTest { simpleTest {
machine = machine =
{ config, pkgs, ... }: { config, pkgs, ... }:
@ -178,6 +186,7 @@ in rec {
services.postgresql.package = pkgs.postgresql92; services.postgresql.package = pkgs.postgresql92;
environment.systemPackages = [ hydra pkgs.perlPackages.LWP pkgs.perlPackages.JSON ]; environment.systemPackages = [ hydra pkgs.perlPackages.LWP pkgs.perlPackages.JSON ];
virtualisation.memorySize = 2047; virtualisation.memorySize = 2047;
boot.kernelPackages = pkgs.linuxPackages_3_10;
}; };
testScript = testScript =
@ -204,4 +213,43 @@ in rec {
$machine->mustSucceed("perl ${./tests/api-test.pl} >&2"); $machine->mustSucceed("perl ${./tests/api-test.pl} >&2");
''; '';
}); });
tests.s3backup = genAttrs' (system:
with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
let hydra = builtins.getAttr system build; in # build."${system}"
simpleTest {
machine =
{ config, pkgs, ... }:
{ services.postgresql.enable = true;
services.postgresql.package = pkgs.postgresql92;
environment.systemPackages = [ hydra pkgs.rubyLibs.fakes3 ];
virtualisation.memorySize = 2047;
boot.kernelPackages = pkgs.linuxPackages_3_10;
virtualisation.writableStore = true;
networking.extraHosts = ''
127.0.0.1 hydra.s3.amazonaws.com
'';
};
testScript =
''
$machine->waitForJob("postgresql");
# Initialise the database and the state.
$machine->mustSucceed
( "createdb -O root hydra"
, "psql hydra -f ${hydra}/libexec/hydra/sql/hydra-postgresql.sql"
, "mkdir /var/lib/hydra"
, "mkdir /tmp/jobs"
, "cp ${./tests/s3-backup-test.pl} /tmp/s3-backup-test.pl"
, "cp ${./tests/api-test.nix} /tmp/jobs/default.nix"
);
# start fakes3
$machine->mustSucceed("fakes3 --root /tmp/s3 --port 80 &>/dev/null &");
$machine->waitForOpenPort("80");
$machine->mustSucceed("cd /tmp && LOGNAME=root AWS_ACCESS_KEY_ID=foo AWS_SECRET_ACCESS_KEY=bar HYDRA_DBI='dbi:Pg:dbname=hydra;user=root;' HYDRA_CONFIG=${./tests/s3-backup-test.config} perl -I ${hydra}/libexec/hydra/lib -I ${hydra.perlDeps}/lib/perl5/site_perl ./s3-backup-test.pl >&2");
'';
});
} }

View file

@ -109,6 +109,22 @@ static int queryMetaFieldInt(MetaInfo & meta, const string & name, int def)
} }
static string queryMetaField(MetaInfo & meta, const string & name)
{
string res;
MetaValue value = meta[name];
if (value.type == MetaValue::tpString)
res = value.stringValue;
else if (value.type == MetaValue::tpStrings) {
foreach (Strings::const_iterator, i, value.stringValues) {
if (res.size() != 0) res += ", ";
res += *i;
}
}
return res;
}
static void findJobsWrapped(EvalState & state, XMLWriter & doc, static void findJobsWrapped(EvalState & state, XMLWriter & doc,
const ArgsUsed & argsUsed, const AutoArgs & argsLeft, const ArgsUsed & argsUsed, const AutoArgs & argsLeft,
Value & v, const string & attrPath) Value & v, const string & attrPath)
@ -136,8 +152,9 @@ static void findJobsWrapped(EvalState & state, XMLWriter & doc,
MetaInfo meta = drv.queryMetaInfo(state); MetaInfo meta = drv.queryMetaInfo(state);
xmlAttrs["description"] = queryMetaFieldString(meta, "description"); xmlAttrs["description"] = queryMetaFieldString(meta, "description");
xmlAttrs["longDescription"] = queryMetaFieldString(meta, "longDescription"); xmlAttrs["longDescription"] = queryMetaFieldString(meta, "longDescription");
xmlAttrs["license"] = queryMetaFieldString(meta, "license"); xmlAttrs["license"] = queryMetaField(meta, "license");
xmlAttrs["homepage"] = queryMetaFieldString(meta, "homepage"); xmlAttrs["homepage"] = queryMetaFieldString(meta, "homepage");
xmlAttrs["maintainers"] = queryMetaField(meta, "maintainers");
int prio = queryMetaFieldInt(meta, "schedulingPriority", 100); int prio = queryMetaFieldInt(meta, "schedulingPriority", 100);
xmlAttrs["schedulingPriority"] = int2String(prio); xmlAttrs["schedulingPriority"] = int2String(prio);
@ -148,17 +165,22 @@ static void findJobsWrapped(EvalState & state, XMLWriter & doc,
int maxsilent = queryMetaFieldInt(meta, "maxSilent", 3600); int maxsilent = queryMetaFieldInt(meta, "maxSilent", 3600);
xmlAttrs["maxSilent"] = int2String(maxsilent); xmlAttrs["maxSilent"] = int2String(maxsilent);
string maintainers; /* If this is an aggregate, then get its constituents. */
MetaValue value = meta["maintainers"]; Bindings::iterator a = v.attrs->find(state.symbols.create("_hydraAggregate"));
if (value.type == MetaValue::tpString) if (a != v.attrs->end() && state.forceBool(*a->value)) {
maintainers = value.stringValue; Bindings::iterator a = v.attrs->find(state.symbols.create("constituents"));
else if (value.type == MetaValue::tpStrings) { if (a == v.attrs->end())
foreach (Strings::const_iterator, i, value.stringValues) { throw EvalError("derivation must have a constituents attribute");
if (maintainers.size() != 0) maintainers += ", "; PathSet context;
maintainers += *i; state.coerceToString(*a->value, context, true, false);
PathSet drvs;
foreach (PathSet::iterator, i, context)
if (i->at(0) == '!') {
size_t index = i->find("!", 1);
drvs.insert(string(*i, index + 1));
} }
xmlAttrs["constituents"] = concatStringsSep(" ", drvs);
} }
xmlAttrs["maintainers"] = maintainers;
/* Register the derivation as a GC root. !!! This /* Register the derivation as a GC root. !!! This
registers roots for jobs that we may have already registers roots for jobs that we may have already
@ -267,9 +289,8 @@ void run(Strings args)
store = openStore(); store = openStore();
Expr * e = state.parseExprFromFile(releaseExpr);
Value v; Value v;
state.mkThunk_(v, e); state.evalFile(releaseExpr, v);
XMLWriter doc(true, std::cout); XMLWriter doc(true, std::cout);
XMLOpenElement root(doc, "jobs"); XMLOpenElement root(doc, "jobs");

View file

@ -7,46 +7,6 @@ use Hydra::Helper::Nix;
use Hydra::Helper::CatalystUtils; use Hydra::Helper::CatalystUtils;
sub getJobStatus {
my ($self, $c) = @_;
my $maintainer = $c->request->params->{"maintainer"};
my $latest = $c->stash->{jobStatus}->search(
defined $maintainer ? { maintainers => { like => "%$maintainer%" } } : {},
{ '+select' => ["me.statusChangeId", "me.statusChangeTime"]
, '+as' => ["statusChangeId", "statusChangeTime"]
, order_by => "coalesce(statusChangeTime, 0) desc"
});
return $latest;
}
sub jobstatus : Chained('get_builds') PathPart Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'jobstatus.tt';
$c->stash->{latestBuilds} = [getJobStatus($self, $c)->all];
}
# A convenient way to see all the errors - i.e. things demanding
# attention - at a glance.
sub errors : Chained('get_builds') PathPart Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'errors.tt';
$c->stash->{brokenJobsets} =
[$c->stash->{allJobsets}->search({errormsg => {'!=' => ''}})]
if defined $c->stash->{allJobsets};
$c->stash->{brokenJobs} =
[$c->stash->{allJobs}->search({errormsg => {'!=' => ''}})]
if defined $c->stash->{allJobs};
$c->stash->{brokenBuilds} =
[getJobStatus($self, $c)->search({buildStatus => {'!=' => 0}})];
}
sub all : Chained('get_builds') PathPart { sub all : Chained('get_builds') PathPart {
my ($self, $c) = @_; my ($self, $c) = @_;
@ -56,13 +16,12 @@ sub all : Chained('get_builds') PathPart {
my $resultsPerPage = 20; my $resultsPerPage = 20;
my $nrBuilds = $c->stash->{allBuilds}->search({finished => 1})->count;
$c->stash->{baseUri} = $c->uri_for($self->action_for("all"), $c->req->captures); $c->stash->{baseUri} = $c->uri_for($self->action_for("all"), $c->req->captures);
$c->stash->{page} = $page; $c->stash->{page} = $page;
$c->stash->{resultsPerPage} = $resultsPerPage; $c->stash->{resultsPerPage} = $resultsPerPage;
$c->stash->{total} = $nrBuilds; $c->stash->{total} = $c->stash->{allBuilds}->search({finished => 1})->count
unless defined $c->stash->{total};
$c->stash->{builds} = [ $c->stash->{allBuilds}->search( $c->stash->{builds} = [ $c->stash->{allBuilds}->search(
{ finished => 1 }, { finished => 1 },
@ -82,6 +41,7 @@ sub nix : Chained('get_builds') PathPart('channel') CaptureArgs(1) {
->search_literal("exists (select 1 from buildproducts where build = me.id and type = 'nix-build')") ->search_literal("exists (select 1 from buildproducts where build = me.id and type = 'nix-build')")
->search({}, { columns => [@buildListColumns, 'drvpath', 'description', 'homepage'] ->search({}, { columns => [@buildListColumns, 'drvpath', 'description', 'homepage']
, join => ["buildoutputs"] , join => ["buildoutputs"]
, order_by => ["me.id", "buildoutputs.name"]
, '+select' => ['buildoutputs.path', 'buildoutputs.name'], '+as' => ['outpath', 'outname'] }); , '+select' => ['buildoutputs.path', 'buildoutputs.name'], '+as' => ['outpath', 'outname'] });
} }
else { else {
@ -120,4 +80,22 @@ sub latest_for : Chained('get_builds') PathPart('latest-for') {
} }
# Redirect to the latest successful build in a finished evaluation
# (i.e. an evaluation that has no unfinished builds).
sub latest_finished : Chained('get_builds') PathPart('latest-finished') {
my ($self, $c, @rest) = @_;
my $latest = $c->stash->{allBuilds}->find(
{ finished => 1, buildstatus => 0 },
{ order_by => ["id DESC"], rows => 1, join => ["jobsetevalmembers"]
, where => \
"not exists (select 1 from jobsetevalmembers m2 join builds b2 on jobsetevalmembers.eval = m2.eval and m2.build = b2.id and b2.finished = 0)"
});
notFound($c, "There is no successful build to redirect to.") unless defined $latest;
$c->res->redirect($c->uri_for($c->controller('Build')->action_for("build"), [$latest->id], @rest));
}
1; 1;

View file

@ -14,20 +14,36 @@ sub getChannelData {
my @storePaths = (); my @storePaths = ();
$c->stash->{nixPkgs} = []; $c->stash->{nixPkgs} = [];
foreach my $build ($c->stash->{channelBuilds}->all) {
my $outPath = $build->get_column("outpath"); my @builds = $c->stash->{channelBuilds}->all;
my $outName = $build->get_column("outname");
for (my $n = 0; $n < scalar @builds; ) {
# Since channelData is a join of Builds and BuildOutputs, we
# need to gather the rows that belong to a single build.
my $build = $builds[$n++];
my @outputs = ($build);
push @outputs, $builds[$n++] while $n < scalar @builds && $builds[$n]->id == $build->id;
@outputs = grep { $_->get_column("outpath") } @outputs;
my $outputs = {};
foreach my $output (@outputs) {
my $outPath = $output->get_column("outpath");
next if $checkValidity && !isValidPath($outPath); next if $checkValidity && !isValidPath($outPath);
$outputs->{$output->get_column("outname")} = $outPath;
push @storePaths, $outPath; push @storePaths, $outPath;
my $pkgName = $build->nixname . "-" . $build->system . "-" . $build->id . ($outName ne "out" ? "-" . $outName : ""); # Put the system type in the manifest (for top-level
push @{$c->stash->{nixPkgs}}, { build => $build, name => $pkgName, outPath => $outPath, outName => $outName }; # paths) as a hint to the binary patch generator. (It
# Put the system type in the manifest (for top-level paths) as # shouldn't try to generate patches between builds for
# a hint to the binary patch generator. (It shouldn't try to # different systems.) It would be nice if Nix stored this
# generate patches between builds for different systems.) It # info for every path but it doesn't.
# would be nice if Nix stored this info for every path but it
# doesn't.
$c->stash->{systemForPath}->{$outPath} = $build->system; $c->stash->{systemForPath}->{$outPath} = $build->system;
}; }
next if !%$outputs;
my $pkgName = $build->nixname . "-" . $build->system . "-" . $build->id;
push @{$c->stash->{nixPkgs}}, { build => $build, name => $pkgName, outputs => $outputs };
}
$c->stash->{storePaths} = [@storePaths]; $c->stash->{storePaths} = [@storePaths];
} }

View file

@ -4,8 +4,12 @@ use strict;
use warnings; use warnings;
use base 'Catalyst::Controller::REST'; use base 'Catalyst::Controller::REST';
# Hack: Erase the map set by C::C::REST
__PACKAGE__->config( map => undef );
__PACKAGE__->config( __PACKAGE__->config(
map => { map => {
'application/json' => 'JSON',
'text/x-json' => 'JSON',
'text/html' => [ 'View', 'TT' ] 'text/html' => [ 'View', 'TT' ]
}, },
default => 'text/html', default => 'text/html',

View file

@ -8,36 +8,26 @@ use base 'DBIx::Class';
sub TO_JSON { sub TO_JSON {
my $self = shift; my $self = shift;
my $json = { $self->get_columns };
my $rs = $self->result_source; my $hint = $self->json_hint;
my @relnames = $rs->relationships;
RELLOOP: foreach my $relname (@relnames) { my %json = ();
my $relinfo = $rs->relationship_info($relname);
next unless defined $relinfo->{attrs}->{accessor}; foreach my $column (@{$hint->{columns}}) {
my $accessor = $relinfo->{attrs}->{accessor}; $json{$column} = $self->get_column($column);
if ($accessor eq "single" and exists $self->{_relationship_data}{$relname}) {
$json->{$relname} = $self->$relname->TO_JSON;
} else {
unless (defined $self->{related_resultsets}{$relname}) {
my $cond = $relinfo->{cond};
if (ref $cond eq 'HASH') {
foreach my $k (keys %{$cond}) {
my $v = $cond->{$k};
$v =~ s/^self\.//;
next RELLOOP unless $self->has_column_loaded($v);
} }
} #!!! TODO: Handle ARRAY conditions
foreach my $relname (keys %{$hint->{relations}}) {
my $key = $hint->{relations}->{$relname};
$json{$relname} = [ map { $_->$key } $self->$relname ];
} }
if (defined $self->related_resultset($relname)->get_cache) {
if ($accessor eq "multi") { foreach my $relname (keys %{$hint->{eager_relations}}) {
$json->{$relname} = [ map { $_->TO_JSON } $self->$relname ]; my $key = $hint->{eager_relations}->{$relname};
} else { $json{$relname} = { map { $_->$key => $_ } $self->$relname };
$json->{$relname} = $self->$relname->TO_JSON;
} }
}
} return \%json;
}
return $json;
} }
1; 1;

View file

@ -15,8 +15,6 @@ use Digest::SHA qw(sha256_hex);
use Text::Diff; use Text::Diff;
use File::Slurp; use File::Slurp;
# !!! Rewrite this to use View::JSON.
sub api : Chained('/') PathPart('api') CaptureArgs(0) { sub api : Chained('/') PathPart('api') CaptureArgs(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
@ -24,32 +22,6 @@ sub api : Chained('/') PathPart('api') CaptureArgs(0) {
} }
sub projectToHash {
my ($project) = @_;
return {
name => $project->name,
description => $project->description
};
}
sub projects : Chained('api') PathPart('projects') Args(0) {
my ($self, $c) = @_;
my @projects = $c->model('DB::Projects')->search({hidden => 0}, {order_by => 'name'});
my @list;
foreach my $p (@projects) {
push @list, projectToHash($p);
}
$c->stash->{'plain'} = {
data => scalar (JSON::Any->objToJson(\@list))
};
$c->forward('Hydra::View::Plain');
}
sub buildToHash { sub buildToHash {
my ($build) = @_; my ($build) = @_;
my $result = { my $result = {

View file

@ -34,8 +34,12 @@ sub machines : Chained('admin') PathPart('machines') Args(0) {
sub clear_queue_non_current : Chained('admin') PathPart('clear-queue-non-current') Args(0) { sub clear_queue_non_current : Chained('admin') PathPart('clear-queue-non-current') Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
my $time = time(); my $builds = $c->model('DB::Builds')->search(
$c->model('DB::Builds')->search({finished => 0, iscurrent => 0, busy => 0})->update({ finished => 1, buildstatus => 4, starttime => $time, stoptime => $time }); { finished => 0, busy => 0
, id => { -not_in => \ "select build from JobsetEvalMembers where eval in (select max(id) from JobsetEvals where hasNewBuilds = 1 group by project, jobset)" }
});
my $n = cancelBuilds($c->model('DB')->schema, $builds);
$c->flash->{successMsg} = "$n builds have been cancelled.";
$c->res->redirect($c->request->referer // "/admin"); $c->res->redirect($c->request->referer // "/admin");
} }
@ -49,19 +53,11 @@ sub clearfailedcache : Chained('admin') PathPart('clear-failed-cache') Args(0) {
sub clearvcscache : Chained('admin') PathPart('clear-vcs-cache') Args(0) { sub clearvcscache : Chained('admin') PathPart('clear-vcs-cache') Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
$c->model('DB::CachedPathInputs')->delete;
print STDERR "Clearing path cache\n"; $c->model('DB::CachedGitInputs')->delete;
$c->model('DB::CachedPathInputs')->delete_all; $c->model('DB::CachedSubversionInputs')->delete;
$c->model('DB::CachedBazaarInputs')->delete;
print STDERR "Clearing git cache\n"; $c->flash->{successMsg} = "VCS caches have been cleared.";
$c->model('DB::CachedGitInputs')->delete_all;
print STDERR "Clearing subversion cache\n";
$c->model('DB::CachedSubversionInputs')->delete_all;
print STDERR "Clearing bazaar cache\n";
$c->model('DB::CachedBazaarInputs')->delete_all;
$c->res->redirect($c->request->referer // "/admin"); $c->res->redirect($c->request->referer // "/admin");
} }

View file

@ -35,18 +35,18 @@ sub buildChain :Chained('/') :PathPart('build') :CaptureArgs(1) {
sub findBuildStepByOutPath { sub findBuildStepByOutPath {
my ($self, $c, $path, $status) = @_; my ($self, $c, $path) = @_;
return $c->model('DB::BuildSteps')->search( return $c->model('DB::BuildSteps')->search(
{ path => $path, busy => 0, status => $status }, { path => $path, busy => 0 },
{ join => ["buildstepoutputs"], order_by => ["stopTime"], limit => 1 })->single; { join => ["buildstepoutputs"], order_by => ["status", "stopTime"], rows => 1 })->single;
} }
sub findBuildStepByDrvPath { sub findBuildStepByDrvPath {
my ($self, $c, $drvPath, $status) = @_; my ($self, $c, $drvPath) = @_;
return $c->model('DB::BuildSteps')->search( return $c->model('DB::BuildSteps')->search(
{ drvpath => $drvPath, busy => 0, status => $status }, { drvpath => $drvPath, busy => 0 },
{ order_by => ["stopTime"], limit => 1 })->single; { order_by => ["status", "stopTime"], rows => 1 })->single;
} }
@ -60,7 +60,6 @@ sub build_GET {
$c->stash->{template} = 'build.tt'; $c->stash->{template} = 'build.tt';
$c->stash->{available} = all { isValidPath($_->path) } $build->buildoutputs->all; $c->stash->{available} = all { isValidPath($_->path) } $build->buildoutputs->all;
$c->stash->{drvAvailable} = isValidPath $build->drvpath; $c->stash->{drvAvailable} = isValidPath $build->drvpath;
$c->stash->{flashMsg} = $c->flash->{buildMsg};
if (!$build->finished && $build->busy) { if (!$build->finished && $build->busy) {
$c->stash->{logtext} = read_file($build->logfile, err_mode => 'quiet') // ""; $c->stash->{logtext} = read_file($build->logfile, err_mode => 'quiet') // "";
@ -68,8 +67,7 @@ sub build_GET {
if ($build->finished && $build->iscachedbuild) { if ($build->finished && $build->iscachedbuild) {
my $path = ($build->buildoutputs)[0]->path or die; my $path = ($build->buildoutputs)[0]->path or die;
my $cachedBuildStep = findBuildStepByOutPath($self, $c, $path, my $cachedBuildStep = findBuildStepByOutPath($self, $c, $path);
$build->buildstatus == 0 || $build->buildstatus == 6 ? 0 : 1);
$c->stash->{cachedBuild} = $cachedBuildStep->build if defined $cachedBuildStep; $c->stash->{cachedBuild} = $cachedBuildStep->build if defined $cachedBuildStep;
} }
@ -95,25 +93,16 @@ sub build_GET {
# Get the first eval of which this build was a part. # Get the first eval of which this build was a part.
($c->stash->{nrEvals}) = $c->stash->{build}->jobsetevals->search({ hasnewbuilds => 1 })->count; ($c->stash->{nrEvals}) = $c->stash->{build}->jobsetevals->search({ hasnewbuilds => 1 })->count;
($c->stash->{eval}) = $c->stash->{build}->jobsetevals->search( $c->stash->{eval} = $c->stash->{build}->jobsetevals->search(
{ hasnewbuilds => 1}, { hasnewbuilds => 1},
{ limit => 1, order_by => ["id"] }); { rows => 1, order_by => ["id"] })->single;
$self->status_ok( $self->status_ok(
$c, $c,
entity => $c->model('DB::Builds')->find($build->id,{ entity => $build
columns => [
'id',
'finished',
'timestamp',
'buildstatus',
'job',
'project',
'jobset',
'starttime',
'stoptime',
]
})
); );
# If this is an aggregate build, get its constituents.
$c->stash->{constituents} = [$c->stash->{build}->constituents_->search({}, {order_by => ["job"]})];
} }
@ -125,36 +114,44 @@ sub view_nixlog : Chained('buildChain') PathPart('nixlog') {
$c->stash->{step} = $step; $c->stash->{step} = $step;
showLog($c, $step->drvpath, $mode); showLog($c, $mode, $step->drvpath, map { $_->path } $step->buildstepoutputs->all);
} }
sub view_log : Chained('buildChain') PathPart('log') { sub view_log : Chained('buildChain') PathPart('log') {
my ($self, $c, $mode) = @_; my ($self, $c, $mode) = @_;
showLog($c, $c->stash->{build}->drvpath, $mode); showLog($c, $mode, $c->stash->{build}->drvpath, map { $_->path } $c->stash->{build}->buildoutputs->all);
} }
sub showLog { sub showLog {
my ($c, $drvPath, $mode) = @_; my ($c, $mode, $drvPath, @outPaths) = @_;
my $logPath = getDrvLogPath($drvPath); my $logPath = findLog($c, $drvPath, @outPaths);
notFound($c, "The build log of derivation $drvPath is not available.") unless defined $logPath; notFound($c, "The build log of derivation $drvPath is not available.") unless defined $logPath;
my $size = stat($logPath)->size;
error($c, "This build log is too big to display ($size bytes).")
if $size >= 64 * 1024 * 1024;
if (!$mode) { if (!$mode) {
# !!! quick hack # !!! quick hack
my $pipeline = "nix-store -l $drvPath" my $pipeline = ($logPath =~ /.bz2$/ ? "bzip2 -d < $logPath" : "cat $logPath")
. " | nix-log2xml | xsltproc " . $c->path_to("xsl/mark-errors.xsl") . " -" . " | nix-log2xml | xsltproc " . $c->path_to("xsl/mark-errors.xsl") . " -"
. " | xsltproc " . $c->path_to("xsl/log2html.xsl") . " - | tail -n +2"; . " | xsltproc " . $c->path_to("xsl/log2html.xsl") . " - | tail -n +2";
$c->stash->{template} = 'log.tt'; $c->stash->{template} = 'log.tt';
$c->stash->{logtext} = `$pipeline`; $c->stash->{logtext} = `ulimit -t 5 ; $pipeline`;
} }
elsif ($mode eq "raw") { elsif ($mode eq "raw") {
$c->stash->{'plain'} = { data => (scalar logContents($drvPath)) || " " }; if ($logPath !~ /.bz2$/) {
$c->serve_static_file($logPath);
} else {
$c->stash->{'plain'} = { data => (scalar logContents($logPath)) || " " };
$c->forward('Hydra::View::Plain'); $c->forward('Hydra::View::Plain');
} }
}
elsif ($mode eq "tail-reload") { elsif ($mode eq "tail-reload") {
my $url = $c->request->uri->as_string; my $url = $c->request->uri->as_string;
@ -162,12 +159,12 @@ sub showLog {
$c->stash->{url} = $url; $c->stash->{url} = $url;
$c->stash->{reload} = !$c->stash->{build}->finished && $c->stash->{build}->busy; $c->stash->{reload} = !$c->stash->{build}->finished && $c->stash->{build}->busy;
$c->stash->{title} = ""; $c->stash->{title} = "";
$c->stash->{contents} = (scalar logContents($drvPath, 50)) || " "; $c->stash->{contents} = (scalar logContents($logPath, 50)) || " ";
$c->stash->{template} = 'plain-reload.tt'; $c->stash->{template} = 'plain-reload.tt';
} }
elsif ($mode eq "tail") { elsif ($mode eq "tail") {
$c->stash->{'plain'} = { data => (scalar logContents($drvPath, 50)) || " " }; $c->stash->{'plain'} = { data => (scalar logContents($logPath, 50)) || " " };
$c->forward('Hydra::View::Plain'); $c->forward('Hydra::View::Plain');
} }
@ -238,6 +235,21 @@ sub download : Chained('buildChain') PathPart {
} }
sub output : Chained('buildChain') PathPart Args(1) {
my ($self, $c, $outputName) = @_;
my $build = $c->stash->{build};
error($c, "This build is not finished yet.") unless $build->finished;
my $output = $build->buildoutputs->find({name => $outputName});
notFound($c, "This build has no output named $outputName") unless defined $output;
error($c, "Output is not available.") unless isValidPath $output->path;
$c->response->header('Content-Disposition', "attachment; filename=\"build-${\$build->id}-${\$outputName}.nar.bz2\"");
$c->stash->{current_view} = 'NixNAR';
$c->stash->{storePath} = $output->path;
}
# Redirect to a download with the given type. Useful when you want to # Redirect to a download with the given type. Useful when you want to
# link to some build product of the latest build (i.e. in conjunction # link to some build product of the latest build (i.e. in conjunction
# with the .../latest redirect). # with the .../latest redirect).
@ -269,7 +281,7 @@ sub contents : Chained('buildChain') PathPart Args(1) {
notFound($c, "Product $path has disappeared.") unless -e $path; notFound($c, "Product $path has disappeared.") unless -e $path;
# Sanitize $path to prevent shell injection attacks. # Sanitize $path to prevent shell injection attacks.
$path =~ /^\/[\/[A-Za-z0-9_\-\.=]+$/ or die "Filename contains illegal characters.\n"; $path =~ /^\/[\/[A-Za-z0-9_\-\.=+:]+$/ or die "Filename contains illegal characters.\n";
# FIXME: don't use shell invocations below. # FIXME: don't use shell invocations below.
@ -339,8 +351,8 @@ sub getDependencyGraph {
{ path => $path { path => $path
, name => $name , name => $name
, buildStep => $runtime , buildStep => $runtime
? findBuildStepByOutPath($self, $c, $path, 0) ? findBuildStepByOutPath($self, $c, $path)
: findBuildStepByDrvPath($self, $c, $path, 0) : findBuildStepByDrvPath($self, $c, $path)
}; };
$$done{$path} = $node; $$done{$path} = $node;
my @refs; my @refs;
@ -409,49 +421,22 @@ sub nix : Chained('buildChain') PathPart('nix') CaptureArgs(0) {
sub restart : Chained('buildChain') PathPart Args(0) { sub restart : Chained('buildChain') PathPart Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
my $build = $c->stash->{build}; my $build = $c->stash->{build};
requireProjectOwner($c, $build->project); requireProjectOwner($c, $build->project);
my $n = restartBuilds($c->model('DB')->schema, $c->model('DB::Builds')->search({ id => $build->id }));
my $drvpath = $build->drvpath; error($c, "This build cannot be restarted.") if $n != 1;
error($c, "This build cannot be restarted.") $c->flash->{successMsg} = "Build has been restarted.";
unless $build->finished && -f $drvpath;
restartBuild($c->model('DB')->schema, $build);
$c->flash->{buildMsg} = "Build has been restarted.";
$c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures)); $c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures));
} }
sub cancel : Chained('buildChain') PathPart Args(0) { sub cancel : Chained('buildChain') PathPart Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
my $build = $c->stash->{build}; my $build = $c->stash->{build};
requireProjectOwner($c, $build->project); requireProjectOwner($c, $build->project);
my $n = cancelBuilds($c->model('DB')->schema, $c->model('DB::Builds')->search({ id => $build->id }));
txn_do($c->model('DB')->schema, sub { error($c, "This build cannot be cancelled.") if $n != 1;
error($c, "This build cannot be cancelled.") $c->flash->{successMsg} = "Build has been cancelled.";
if $build->finished || $build->busy;
# !!! Actually, it would be nice to be able to cancel busy
# builds as well, but we would have to send a signal or
# something to the build process.
my $time = time();
$build->update(
{ finished => 1, busy => 0
, iscachedbuild => 0, buildstatus => 4 # = cancelled
, starttime => $time
, stoptime => $time
});
});
$c->flash->{buildMsg} = "Build has been cancelled.";
$c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures)); $c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures));
} }
@ -472,7 +457,7 @@ sub keep : Chained('buildChain') PathPart Args(1) {
$build->update({keep => $keep}); $build->update({keep => $keep});
}); });
$c->flash->{buildMsg} = $c->flash->{successMsg} =
$keep ? "Build will be kept." : "Build will not be kept."; $keep ? "Build will be kept." : "Build will not be kept.";
$c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures)); $c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures));
@ -502,89 +487,12 @@ sub add_to_release : Chained('buildChain') PathPart('add-to-release') Args(0) {
$release->releasemembers->create({build => $build->id, description => $build->description}); $release->releasemembers->create({build => $build->id, description => $build->description});
$c->flash->{buildMsg} = "Build added to project <tt>$releaseName</tt>."; $c->flash->{successMsg} = "Build added to project <tt>$releaseName</tt>.";
$c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures)); $c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures));
} }
sub clone : Chained('buildChain') PathPart('clone') Args(0) {
my ($self, $c) = @_;
my $build = $c->stash->{build};
requireProjectOwner($c, $build->project);
$c->stash->{template} = 'clone-build.tt';
}
sub clone_submit : Chained('buildChain') PathPart('clone/submit') Args(0) {
my ($self, $c) = @_;
my $build = $c->stash->{build};
requireProjectOwner($c, $build->project);
my ($nixExprPath, $nixExprInputName) = Hydra::Controller::Jobset::nixExprPathFromParams $c;
# When the expression is in a .scm file, assume it's a Guile + Guix
# build expression.
my $exprType =
$c->request->params->{"nixexprpath"} =~ /.scm$/ ? "guile" : "nix";
my $jobName = trim $c->request->params->{"jobname"};
error($c, "Invalid job name: $jobName") if $jobName !~ /^$jobNameRE$/;
my $inputInfo = {};
foreach my $param (keys %{$c->request->params}) {
next unless $param =~ /^input-(\w+)-name$/;
my $baseName = $1;
my ($inputName, $inputType) =
Hydra::Controller::Jobset::checkInput($c, $baseName);
my $inputValue = Hydra::Controller::Jobset::checkInputValue(
$c, $inputType, $c->request->params->{"input-$baseName-value"});
eval {
# !!! fetchInput can take a long time, which might cause
# the current HTTP request to time out. So maybe this
# should be done asynchronously. But then error reporting
# becomes harder.
my $info = fetchInput(
$c->hydra_plugins, $c->model('DB'), $build->project, $build->jobset,
$inputName, $inputType, $inputValue);
push @{$$inputInfo{$inputName}}, $info if defined $info;
};
error($c, $@) if $@;
}
my ($jobs, $nixExprInput) = evalJobs($inputInfo, $exprType, $nixExprInputName, $nixExprPath);
my $job;
foreach my $j (@{$jobs->{job}}) {
print STDERR $j->{jobName}, "\n";
if ($j->{jobName} eq $jobName) {
error($c, "Nix expression returned multiple builds for job $jobName.")
if $job;
$job = $j;
}
}
error($c, "Nix expression did not return a job named $jobName.") unless $job;
my %currentBuilds;
my $newBuild = checkBuild(
$c->model('DB'), $build->project, $build->jobset,
$inputInfo, $nixExprInput, $job, \%currentBuilds, undef, {});
error($c, "This build has already been performed.") unless $newBuild;
$c->flash->{buildMsg} = "Build " . $newBuild->id . " added to the queue.";
$c->res->redirect($c->uri_for($c->controller('Root')->action_for('queue')));
}
sub get_info : Chained('buildChain') PathPart('api/get-info') Args(0) { sub get_info : Chained('buildChain') PathPart('api/get-info') Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
my $build = $c->stash->{build}; my $build = $c->stash->{build};
@ -614,6 +522,22 @@ sub evals : Chained('buildChain') PathPart('evals') Args(0) {
} }
# Redirect to the latest finished evaluation that contains this build.
sub eval : Chained('buildChain') PathPart('eval') {
my ($self, $c, @rest) = @_;
my $eval = $c->stash->{build}->jobsetevals->find(
{ hasnewbuilds => 1 },
{ order_by => "id DESC", rows => 1
, "not exists (select 1 from jobsetevalmembers m2 join builds b2 on me.eval = m2.eval and m2.build = b2.id and b2.finished = 0)"
});
notFound($c, "There is no finished evaluation containing this build.") unless defined $eval;
$c->res->redirect($c->uri_for($c->controller('JobsetEval')->action_for("view"), [$eval->id], @rest, $c->req->params));
}
sub reproduce : Chained('buildChain') PathPart('reproduce') Args(0) { sub reproduce : Chained('buildChain') PathPart('reproduce') Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
$c->response->content_type('text/x-shellscript'); $c->response->content_type('text/x-shellscript');

View file

@ -20,24 +20,52 @@ sub job : Chained('/') PathPart('job') CaptureArgs(3) {
sub overview : Chained('job') PathPart('') Args(0) { sub overview : Chained('job') PathPart('') Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
my $job = $c->stash->{job};
$c->stash->{template} = 'job.tt'; $c->stash->{template} = 'job.tt';
$c->stash->{lastBuilds} = $c->stash->{lastBuilds} =
[ $c->stash->{job}->builds->search({ finished => 1 }, [ $job->builds->search({ finished => 1 },
{ order_by => 'id DESC', rows => 10, columns => [@buildListColumns] }) ]; { order_by => 'id DESC', rows => 10, columns => [@buildListColumns] }) ];
$c->stash->{queuedBuilds} = [ $c->stash->{queuedBuilds} = [
$c->stash->{job}->builds->search( $job->builds->search(
{ finished => 0 }, { finished => 0 },
{ join => ['project'] { order_by => ["priority DESC", "id"] }
, order_by => ["priority DESC", "id"]
, '+select' => ['project.enabled']
, '+as' => ['enabled']
}
) ]; ) ];
$c->stash->{systems} = [$c->stash->{job}->builds->search({iscurrent => 1}, {select => ["system"], distinct => 1})]; # If this is an aggregate job, then get its constituents.
my @constituents = $c->model('DB::Builds')->search(
{ aggregate => { -in => $job->builds->search({}, { columns => ["id"], order_by => "id desc", rows => 15 })->as_query } },
{ join => 'aggregateconstituents_constituents',
columns => ['id', 'job', 'finished', 'buildstatus'],
+select => ['aggregateconstituents_constituents.aggregate'],
+as => ['aggregate']
});
my $aggregates = {};
my %constituentJobs;
foreach my $b (@constituents) {
my $jobName = $b->get_column('job');
$aggregates->{$b->get_column('aggregate')}->{constituents}->{$jobName} =
{ id => $b->id, finished => $b->finished, buildstatus => $b->buildstatus };
$constituentJobs{$jobName} = 1;
}
foreach my $agg (keys %$aggregates) {
# FIXME: could be done in one query.
$aggregates->{$agg}->{build} =
$c->model('DB::Builds')->find({id => $agg}, {columns => [@buildListColumns]}) or die;
}
$c->stash->{aggregates} = $aggregates;
$c->stash->{constituentJobs} = [sort (keys %constituentJobs)];
$c->stash->{starred} = $c->user->starredjobs(
{ project => $c->stash->{project}->name
, jobset => $c->stash->{jobset}->name
, job => $c->stash->{job}->name
})->count == 1 if $c->user_exists;
} }
@ -45,9 +73,6 @@ sub overview : Chained('job') PathPart('') Args(0) {
sub get_builds : Chained('job') PathPart('') CaptureArgs(0) { sub get_builds : Chained('job') PathPart('') CaptureArgs(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
$c->stash->{allBuilds} = $c->stash->{job}->builds; $c->stash->{allBuilds} = $c->stash->{job}->builds;
$c->stash->{jobStatus} = $c->model('DB')->resultset('JobStatusForJob')
->search({}, {bind => [$c->stash->{project}->name, $c->stash->{jobset}->name, $c->stash->{job}->name]});
$c->stash->{allJobs} = $c->stash->{job_};
$c->stash->{latestSucceeded} = $c->model('DB')->resultset('LatestSucceededForJob') $c->stash->{latestSucceeded} = $c->model('DB')->resultset('LatestSucceededForJob')
->search({}, {bind => [$c->stash->{project}->name, $c->stash->{jobset}->name, $c->stash->{job}->name]}); ->search({}, {bind => [$c->stash->{project}->name, $c->stash->{jobset}->name, $c->stash->{job}->name]});
$c->stash->{channelBaseName} = $c->stash->{channelBaseName} =
@ -55,4 +80,22 @@ sub get_builds : Chained('job') PathPart('') CaptureArgs(0) {
} }
sub star : Chained('job') PathPart('star') Args(0) {
my ($self, $c) = @_;
requirePost($c);
requireUser($c);
my $args =
{ project => $c->stash->{project}->name
, jobset => $c->stash->{jobset}->name
, job => $c->stash->{job}->name
};
if ($c->request->params->{star} eq "1") {
$c->user->starredjobs->update_or_create($args);
} else {
$c->user->starredjobs->find($args)->delete;
}
$c->stash->{resource}->{success} = 1;
}
1; 1;

View file

@ -1,5 +1,6 @@
package Hydra::Controller::Jobset; package Hydra::Controller::Jobset;
use utf8;
use strict; use strict;
use warnings; use warnings;
use base 'Hydra::Base::Controller::ListBuilds'; use base 'Hydra::Base::Controller::ListBuilds';
@ -9,35 +10,18 @@ use Hydra::Helper::CatalystUtils;
sub jobsetChain :Chained('/') :PathPart('jobset') :CaptureArgs(2) { sub jobsetChain :Chained('/') :PathPart('jobset') :CaptureArgs(2) {
my ($self, $c, $projectName, $jobsetName) = @_; my ($self, $c, $projectName, $jobsetName) = @_;
$c->stash->{params}->{name} //= $jobsetName;
my $project = $c->model('DB::Projects')->find($projectName); my $project = $c->model('DB::Projects')->find($projectName);
if ($project) { notFound($c, "Project $projectName doesn't exist.") if !$project;
$c->stash->{project} = $project; $c->stash->{project} = $project;
$c->stash->{jobset_} = $project->jobsets->search({'me.name' => $jobsetName}); $c->stash->{jobset} = $project->jobsets->find({ name => $jobsetName });
my $jobset = $c->stash->{jobset_}->single;
if ($jobset) { notFound($c, "Jobset $jobsetName doesn't exist.")
$c->stash->{jobset} = $jobset; if !$c->stash->{jobset} && !($c->action->name eq "jobset" and $c->request->method eq "PUT");
} else {
if ($c->action->name eq "jobset" and $c->request->method eq "PUT") {
$c->stash->{jobsetName} = $jobsetName;
} else {
$self->status_not_found(
$c,
message => "Jobset $jobsetName doesn't exist."
);
$c->detach;
}
}
} else {
$self->status_not_found(
$c,
message => "Project $projectName doesn't exist."
);
$c->detach;
}
} }
@ -50,26 +34,11 @@ sub jobset_GET {
$c->stash->{evals} = getEvals($self, $c, scalar $c->stash->{jobset}->jobsetevals, 0, 10); $c->stash->{evals} = getEvals($self, $c, scalar $c->stash->{jobset}->jobsetevals, 0, 10);
($c->stash->{latestEval}) = $c->stash->{jobset}->jobsetevals->search({}, { limit => 1, order_by => ["id desc"] }); $c->stash->{latestEval} = $c->stash->{jobset}->jobsetevals->search({}, { rows => 1, order_by => ["id desc"] })->single;
$self->status_ok( $c->stash->{totalShares} = getTotalShares($c->model('DB')->schema);
$c,
entity => $c->stash->{jobset_}->find({}, { $self->status_ok($c, entity => $c->stash->{jobset});
columns => [
'me.name',
'me.project',
'me.errormsg',
'jobsetinputs.name',
{
'jobsetinputs.jobsetinputalts.altnr' => 'jobsetinputalts.altnr',
'jobsetinputs.jobsetinputalts.value' => 'jobsetinputalts.value'
}
],
join => { 'jobsetinputs' => 'jobsetinputalts' },
collapse => 1,
order_by => "me.name"
})
);
} }
sub jobset_PUT { sub jobset_PUT {
@ -78,133 +47,91 @@ sub jobset_PUT {
requireProjectOwner($c, $c->stash->{project}); requireProjectOwner($c, $c->stash->{project});
if (defined $c->stash->{jobset}) { if (defined $c->stash->{jobset}) {
error($c, "Cannot rename jobset `$c->stash->{params}->{oldName}' over existing jobset `$c->stash->{jobset}->name") if defined $c->stash->{params}->{oldName} and $c->stash->{params}->{oldName} ne $c->stash->{jobset}->name;
txn_do($c->model('DB')->schema, sub { txn_do($c->model('DB')->schema, sub {
updateJobset($c, $c->stash->{jobset}); updateJobset($c, $c->stash->{jobset});
}); });
if ($c->req->looks_like_browser) { my $uri = $c->uri_for($self->action_for("jobset"), [$c->stash->{project}->name, $c->stash->{jobset}->name]) . "#tabs-configuration";
$c->res->redirect($c->uri_for($self->action_for("jobset"), $self->status_ok($c, entity => { redirect => "$uri" });
[$c->stash->{project}->name, $c->stash->{jobset}->name]) . "#tabs-configuration");
} else { $c->flash->{successMsg} = "The jobset configuration has been updated.";
$self->status_no_content($c);
} }
} elsif (defined $c->stash->{params}->{oldName}) {
my $jobset = $c->stash->{project}->jobsets->find({'me.name' => $c->stash->{params}->{oldName}});
if (defined $jobset) {
txn_do($c->model('DB')->schema, sub {
updateJobset($c, $jobset);
});
my $uri = $c->uri_for($self->action_for("jobset"), [$c->stash->{project}->name, $jobset->name]);
if ($c->req->looks_like_browser) {
$c->res->redirect($uri . "#tabs-configuration");
} else {
$self->status_created(
$c,
location => "$uri",
entity => { name => $jobset->name, uri => "$uri", type => "jobset" }
);
}
} else {
$self->status_not_found(
$c,
message => "Jobset $c->stash->{params}->{oldName} doesn't exist."
);
}
} else {
my $exprType =
$c->stash->{params}->{"nixexprpath"} =~ /.scm$/ ? "guile" : "nix";
error($c, "Invalid jobset name: $c->stash->{jobsetName}") if $c->stash->{jobsetName} !~ /^$jobsetNameRE$/;
else {
my $jobset; my $jobset;
txn_do($c->model('DB')->schema, sub { txn_do($c->model('DB')->schema, sub {
# Note: $jobsetName is validated in updateProject, which will # Note: $jobsetName is validated in updateProject, which will
# abort the transaction if the name isn't valid. # abort the transaction if the name isn't valid.
$jobset = $c->stash->{project}->jobsets->create( $jobset = $c->stash->{project}->jobsets->create(
{name => $c->stash->{jobsetName}, nixexprinput => "", nixexprpath => "", emailoverride => ""}); {name => ".tmp", nixexprinput => "", nixexprpath => "", emailoverride => ""});
updateJobset($c, $jobset); updateJobset($c, $jobset);
}); });
my $uri = $c->uri_for($self->action_for("jobset"), [$c->stash->{project}->name, $jobset->name]); my $uri = $c->uri_for($self->action_for("jobset"), [$c->stash->{project}->name, $jobset->name]);
if ($c->req->looks_like_browser) { $self->status_created($c,
$c->res->redirect($uri . "#tabs-configuration");
} else {
$self->status_created(
$c,
location => "$uri", location => "$uri",
entity => { name => $jobset->name, uri => "$uri", type => "jobset" } entity => { name => $jobset->name, uri => "$uri", redirect => "$uri", type => "jobset" });
);
}
} }
} }
sub jobset_DELETE {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
txn_do($c->model('DB')->schema, sub {
$c->stash->{jobset}->jobsetevals->delete;
$c->stash->{jobset}->builds->delete;
$c->stash->{jobset}->delete;
});
my $uri = $c->uri_for($c->controller('Project')->action_for("project"), [$c->stash->{project}->name]);
$self->status_ok($c, entity => { redirect => "$uri" });
$c->flash->{successMsg} = "The jobset has been deleted.";
}
sub jobs_tab : Chained('jobsetChain') PathPart('jobs-tab') Args(0) { sub jobs_tab : Chained('jobsetChain') PathPart('jobs-tab') Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
$c->stash->{template} = 'jobset-jobs-tab.tt'; $c->stash->{template} = 'jobset-jobs-tab.tt';
$c->stash->{activeJobs} = []; $c->stash->{filter} = $c->request->params->{filter} // "";
$c->stash->{inactiveJobs} = []; my $filter = "%" . $c->stash->{filter} . "%";
(my $latestEval) = $c->stash->{jobset}->jobsetevals->search( my @evals = $c->stash->{jobset}->jobsetevals->search({ hasnewbuilds => 1}, { order_by => "id desc", rows => 20 });
{ hasnewbuilds => 1}, { limit => 1, order_by => ["id desc"] });
my %activeJobs; my $evals = {};
if (defined $latestEval) { my %jobs;
foreach my $build ($latestEval->builds->search({}, { order_by => ["job"], select => ["job"] })) { my $nrBuilds = 0;
my $job = $build->get_column("job");
if (!defined $activeJobs{$job}) { foreach my $eval (@evals) {
$activeJobs{$job} = 1; my @builds = $eval->builds->search(
push @{$c->stash->{activeJobs}}, $job; { job => { ilike => $filter } },
} { columns => ['id', 'job', 'finished', 'buildstatus'] });
foreach my $b (@builds) {
my $jobName = $b->get_column('job');
$evals->{$eval->id}->{$jobName} =
{ id => $b->id, finished => $b->finished, buildstatus => $b->buildstatus };
$jobs{$jobName} = 1;
$nrBuilds++;
} }
last if $nrBuilds >= 10000;
} }
foreach my $job ($c->stash->{jobset}->jobs->search({}, { order_by => ["name"] })) { if ($c->request->params->{showInactive}) {
if (!defined $activeJobs{$job->name}) { $c->stash->{showInactive} = 1;
push @{$c->stash->{inactiveJobs}}, $job->name; foreach my $job ($c->stash->{jobset}->jobs->search({ name => { ilike => $filter } })) {
next if defined $jobs{$job->name};
$c->stash->{inactiveJobs}->{$job->name} = $jobs{$job->name} = 1;
} }
} }
}
$c->stash->{evals} = $evals;
sub status_tab : Chained('jobsetChain') PathPart('status-tab') Args(0) { my @jobs = sort (keys %jobs);
my ($self, $c) = @_; $c->stash->{nrJobs} = scalar @jobs;
$c->stash->{template} = 'jobset-status-tab.tt'; splice @jobs, 250 if $c->stash->{filter} eq "";
$c->stash->{jobs} = [@jobs];
# FIXME: use latest eval instead of iscurrent.
$c->stash->{systems} =
[ $c->stash->{jobset}->builds->search({ iscurrent => 1 }, { select => ["system"], distinct => 1, order_by => "system" }) ];
# status per system
my @systems = ();
foreach my $system (@{$c->stash->{systems}}) {
push(@systems, $system->system);
}
my @select = ();
my @as = ();
push(@select, "job"); push(@as, "job");
foreach my $system (@systems) {
push(@select, "(select buildstatus from Builds b where b.id = (select max(id) from Builds t where t.project = me.project and t.jobset = me.jobset and t.job = me.job and t.system = '$system' and t.iscurrent = 1 ))");
push(@as, $system);
push(@select, "(select b.id from Builds b where b.id = (select max(id) from Builds t where t.project = me.project and t.jobset = me.jobset and t.job = me.job and t.system = '$system' and t.iscurrent = 1 ))");
push(@as, "$system-build");
}
$c->stash->{activeJobsStatus} = [
$c->model('DB')->resultset('ActiveJobsForJobset')->search(
{},
{ bind => [$c->stash->{project}->name, $c->stash->{jobset}->name]
, select => \@select
, as => \@as
, order_by => ["job"]
}) ];
} }
@ -212,10 +139,6 @@ sub status_tab : Chained('jobsetChain') PathPart('status-tab') Args(0) {
sub get_builds : Chained('jobsetChain') PathPart('') CaptureArgs(0) { sub get_builds : Chained('jobsetChain') PathPart('') CaptureArgs(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
$c->stash->{allBuilds} = $c->stash->{jobset}->builds; $c->stash->{allBuilds} = $c->stash->{jobset}->builds;
$c->stash->{jobStatus} = $c->model('DB')->resultset('JobStatusForJobset')
->search({}, {bind => [$c->stash->{project}->name, $c->stash->{jobset}->name]});
$c->stash->{allJobsets} = $c->stash->{jobset_};
$c->stash->{allJobs} = $c->stash->{jobset}->jobs;
$c->stash->{latestSucceeded} = $c->model('DB')->resultset('LatestSucceededForJobset') $c->stash->{latestSucceeded} = $c->model('DB')->resultset('LatestSucceededForJobset')
->search({}, {bind => [$c->stash->{project}->name, $c->stash->{jobset}->name]}); ->search({}, {bind => [$c->stash->{project}->name, $c->stash->{jobset}->name]});
$c->stash->{channelBaseName} = $c->stash->{channelBaseName} =
@ -230,31 +153,8 @@ sub edit : Chained('jobsetChain') PathPart Args(0) {
$c->stash->{template} = 'edit-jobset.tt'; $c->stash->{template} = 'edit-jobset.tt';
$c->stash->{edit} = 1; $c->stash->{edit} = 1;
} $c->stash->{clone} = defined $c->stash->{params}->{clone};
$c->stash->{totalShares} = getTotalShares($c->model('DB')->schema);
sub submit : Chained('jobsetChain') PathPart Args(0) {
my ($self, $c) = @_;
requirePost($c);
if (($c->request->params->{submit} // "") eq "delete") {
txn_do($c->model('DB')->schema, sub {
$c->stash->{jobset}->jobsetevals->delete_all;
$c->stash->{jobset}->builds->delete_all;
$c->stash->{jobset}->delete;
});
return $c->res->redirect($c->uri_for($c->controller('Project')->action_for("project"), [$c->stash->{project}->name]));
}
my $newName = trim $c->stash->{params}->{name};
my $oldName = trim $c->stash->{jobset}->name;
unless ($oldName eq $newName) {
$c->stash->{params}->{oldName} = $oldName;
$c->stash->{jobsetName} = $newName;
undef $c->stash->{jobset};
}
jobset_PUT($self, $c);
} }
@ -263,10 +163,10 @@ sub nixExprPathFromParams {
# The Nix expression path must be relative and can't contain ".." elements. # The Nix expression path must be relative and can't contain ".." elements.
my $nixExprPath = trim $c->stash->{params}->{"nixexprpath"}; my $nixExprPath = trim $c->stash->{params}->{"nixexprpath"};
error($c, "Invalid Nix expression path: $nixExprPath") if $nixExprPath !~ /^$relPathRE$/; error($c, "Invalid Nix expression path $nixExprPath.") if $nixExprPath !~ /^$relPathRE$/;
my $nixExprInput = trim $c->stash->{params}->{"nixexprinput"}; my $nixExprInput = trim $c->stash->{params}->{"nixexprinput"};
error($c, "Invalid Nix expression input name: $nixExprInput") unless $nixExprInput =~ /^\w+$/; error($c, "Invalid Nix expression input name $nixExprInput.") unless $nixExprInput =~ /^[[:alpha:]][\w-]*$/;
return ($nixExprPath, $nixExprInput); return ($nixExprPath, $nixExprInput);
} }
@ -275,7 +175,7 @@ sub nixExprPathFromParams {
sub checkInputValue { sub checkInputValue {
my ($c, $type, $value) = @_; my ($c, $type, $value) = @_;
$value = trim $value; $value = trim $value;
error($c, "Invalid Boolean value: $value") if error($c, "Invalid Boolean value $value.") if
$type eq "boolean" && !($value eq "true" || $value eq "false"); $type eq "boolean" && !($value eq "true" || $value eq "false");
return $value; return $value;
} }
@ -284,8 +184,11 @@ sub checkInputValue {
sub updateJobset { sub updateJobset {
my ($c, $jobset) = @_; my ($c, $jobset) = @_;
my $jobsetName = $c->stash->{jobsetName} // $jobset->name; my $jobsetName = $c->stash->{params}->{name};
error($c, "Invalid jobset name: $jobsetName") if $jobsetName !~ /^$jobsetNameRE$/; error($c, "Invalid jobset identifier $jobsetName.") if $jobsetName !~ /^$jobsetNameRE$/;
error($c, "Cannot rename jobset to $jobsetName since that identifier is already taken.")
if $jobsetName ne $jobset->name && defined $c->stash->{project}->jobsets->find({ name => $jobsetName });
# When the expression is in a .scm file, assume it's a Guile + Guix # When the expression is in a .scm file, assume it's a Guile + Guix
# build expression. # build expression.
@ -294,118 +197,61 @@ sub updateJobset {
my ($nixExprPath, $nixExprInput) = nixExprPathFromParams $c; my ($nixExprPath, $nixExprInput) = nixExprPathFromParams $c;
my $enabled = int($c->stash->{params}->{enabled});
die if $enabled < 0 || $enabled > 2;
$jobset->update( $jobset->update(
{ name => $jobsetName { name => $jobsetName
, description => trim($c->stash->{params}->{"description"}) , description => trim($c->stash->{params}->{"description"})
, nixexprpath => $nixExprPath , nixexprpath => $nixExprPath
, nixexprinput => $nixExprInput , nixexprinput => $nixExprInput
, enabled => defined $c->stash->{params}->{enabled} ? 1 : 0 , enabled => $enabled
, enableemail => defined $c->stash->{params}->{enableemail} ? 1 : 0 , enableemail => defined $c->stash->{params}->{enableemail} ? 1 : 0
, emailoverride => trim($c->stash->{params}->{emailoverride}) || "" , emailoverride => trim($c->stash->{params}->{emailoverride}) || ""
, hidden => defined $c->stash->{params}->{visible} ? 0 : 1 , hidden => defined $c->stash->{params}->{visible} ? 0 : 1
, keepnr => int(trim($c->stash->{params}->{keepnr})) || 3 , keepnr => int(trim($c->stash->{params}->{keepnr}))
, checkinterval => int(trim($c->stash->{params}->{checkinterval})) , checkinterval => int(trim($c->stash->{params}->{checkinterval}))
, triggertime => $jobset->triggertime // time() , triggertime => $enabled ? $jobset->triggertime // time() : undef
, schedulingshares => int($c->stash->{params}->{schedulingshares})
}); });
# Process the inputs of this jobset. # Set the inputs of this jobset.
unless (defined $c->stash->{params}->{inputs}) { $jobset->jobsetinputs->delete;
$c->stash->{params}->{inputs} = {};
foreach my $param (keys %{$c->stash->{params}}) {
next unless $param =~ /^input-(\w+)-name$/;
my $baseName = $1;
next if $baseName eq "template";
$c->stash->{params}->{inputs}->{$c->stash->{params}->{$param}} = { type => $c->stash->{params}->{"input-$baseName-type"}, values => $c->stash->{params}->{"input-$baseName-values"} };
unless ($baseName =~ /^\d+$/) { # non-numeric base name is an existing entry
$c->stash->{params}->{inputs}->{$c->stash->{params}->{$param}}->{oldName} = $baseName;
}
}
}
foreach my $inputName (keys %{$c->stash->{params}->{inputs}}) { foreach my $name (keys %{$c->stash->{params}->{inputs}}) {
my $inputData = $c->stash->{params}->{inputs}->{$inputName}; my $inputData = $c->stash->{params}->{inputs}->{$name};
error($c, "Invalid input name: $inputName") unless $inputName =~ /^[[:alpha:]]\w*$/; my $type = $inputData->{type};
my $inputType = $inputData->{type};
error($c, "Invalid input type: $inputType") unless
$inputType eq "svn" || $inputType eq "svn-checkout" || $inputType eq "hg" || $inputType eq "tarball" ||
$inputType eq "string" || $inputType eq "path" || $inputType eq "boolean" || $inputType eq "bzr" || $inputType eq "bzr-checkout" ||
$inputType eq "git" || $inputType eq "build" || $inputType eq "sysbuild" ;
my $input;
unless (defined $inputData->{oldName}) {
$input = $jobset->jobsetinputs->update_or_create(
{ name => $inputName
, type => $inputType
});
} else { # it's an existing input
$input = ($jobset->jobsetinputs->search({name => $inputData->{oldName}}))[0];
die unless defined $input;
$input->update({name => $inputName, type => $inputType});
}
# Update the values for this input. Just delete all the
# current ones, then create the new values.
$input->jobsetinputalts->delete_all;
my $values = $inputData->{values}; my $values = $inputData->{values};
$values = [] unless defined $values; my $emailresponsible = defined $inputData->{emailresponsible} ? 1 : 0;
$values = [$values] unless ref($values) eq 'ARRAY';
error($c, "Invalid input name $name.") unless $name =~ /^[[:alpha:]][\w-]*$/;
error($c, "Invalid input type $type.") unless defined $c->stash->{inputTypes}->{$type};
my $input = $jobset->jobsetinputs->create({
name => $name,
type => $type,
emailresponsible => $emailresponsible
});
# Set the values for this input.
my @values = ref($values) eq 'ARRAY' ? @{$values} : ($values);
my $altnr = 0; my $altnr = 0;
foreach my $value (@{$values}) { foreach my $value (@values) {
$value = checkInputValue($c, $inputType, $value); $value = checkInputValue($c, $type, $value);
$input->jobsetinputalts->create({altnr => $altnr++, value => $value}); $input->jobsetinputalts->create({altnr => $altnr++, value => $value});
} }
} }
# Get rid of deleted inputs.
my @inputs = $jobset->jobsetinputs->all;
foreach my $input (@inputs) {
$input->delete unless defined $c->stash->{params}->{inputs}->{$input->name};
}
} }
sub clone : Chained('jobsetChain') PathPart('clone') Args(0) { sub clone : Chained('jobsetChain') PathPart('clone') Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
my $jobset = $c->stash->{jobset}; requireProjectOwner($c, $c->stash->{project});
requireProjectOwner($c, $jobset->project);
$c->stash->{template} = 'clone-jobset.tt'; $c->stash->{template} = 'edit-jobset.tt';
} $c->stash->{clone} = 1;
$c->stash->{totalShares} = getTotalShares($c->model('DB')->schema);
sub clone_submit : Chained('jobsetChain') PathPart('clone/submit') Args(0) {
my ($self, $c) = @_;
my $jobset = $c->stash->{jobset};
requireProjectOwner($c, $jobset->project);
requirePost($c);
my $newJobsetName = trim $c->stash->{params}->{"newjobset"};
error($c, "Invalid jobset name: $newJobsetName") unless $newJobsetName =~ /^[[:alpha:]][\w\-]*$/;
my $newJobset;
txn_do($c->model('DB')->schema, sub {
$newJobset = $jobset->project->jobsets->create(
{ name => $newJobsetName
, description => $jobset->description
, nixexprpath => $jobset->nixexprpath
, nixexprinput => $jobset->nixexprinput
, enabled => 0
, enableemail => $jobset->enableemail
, emailoverride => $jobset->emailoverride || ""
});
foreach my $input ($jobset->jobsetinputs) {
my $newinput = $newJobset->jobsetinputs->create({name => $input->name, type => $input->type});
foreach my $inputalt ($input->jobsetinputalts) {
$newinput->jobsetinputalts->create({altnr => $inputalt->altnr, value => $inputalt->value});
}
}
});
$c->res->redirect($c->uri_for($c->controller('Jobset')->action_for("edit"), [$jobset->project->name, $newJobsetName]));
} }
@ -428,24 +274,7 @@ sub evals_GET {
my $offset = ($page - 1) * $resultsPerPage; my $offset = ($page - 1) * $resultsPerPage;
$c->stash->{evals} = getEvals($self, $c, $evals, $offset, $resultsPerPage); $c->stash->{evals} = getEvals($self, $c, $evals, $offset, $resultsPerPage);
my %entity = ( my %entity = (
evals => [ $evals->search({ 'me.hasnewbuilds' => 1 }, { evals => [ map { $_->{eval} } @{$c->stash->{evals}} ],
columns => [
'me.hasnewbuilds',
'me.id',
'jobsetevalinputs.name',
'jobsetevalinputs.altnr',
'jobsetevalinputs.revision',
'jobsetevalinputs.type',
'jobsetevalinputs.uri',
'jobsetevalinputs.dependency',
'jobsetevalmembers.build',
],
join => [ 'jobsetevalinputs', 'jobsetevalmembers' ],
collapse => 1,
rows => $resultsPerPage,
offset => $offset,
order_by => "me.id DESC",
}) ],
first => "?page=1", first => "?page=1",
last => "?page=" . POSIX::ceil($c->stash->{total}/$resultsPerPage) last => "?page=" . POSIX::ceil($c->stash->{total}/$resultsPerPage)
); );

View file

@ -26,6 +26,9 @@ sub view : Chained('eval') PathPart('') Args(0) {
my $eval = $c->stash->{eval}; my $eval = $c->stash->{eval};
$c->stash->{filter} = $c->request->params->{filter} // "";
my $filter = $c->stash->{filter} eq "" ? {} : { job => { ilike => "%" . $c->stash->{filter} . "%" } };
my $compare = $c->req->params->{compare}; my $compare = $c->req->params->{compare};
my $eval2; my $eval2;
@ -36,6 +39,11 @@ sub view : Chained('eval') PathPart('') Args(0) {
if ($compare =~ /^\d+$/) { if ($compare =~ /^\d+$/) {
$eval2 = $c->model('DB::JobsetEvals')->find($compare) $eval2 = $c->model('DB::JobsetEvals')->find($compare)
or notFound($c, "Evaluation $compare doesn't exist."); or notFound($c, "Evaluation $compare doesn't exist.");
} elsif ($compare =~ /^-(\d+)$/) {
my $t = int($1);
$eval2 = $c->stash->{jobset}->jobsetevals->find(
{ hasnewbuilds => 1, timestamp => {'<=', $eval->timestamp - $t} },
{ order_by => "timestamp desc", rows => 1});
} elsif (defined $compare && $compare =~ /^($jobsetNameRE)$/) { } elsif (defined $compare && $compare =~ /^($jobsetNameRE)$/) {
my $j = $c->stash->{project}->jobsets->find({name => $compare}) my $j = $c->stash->{project}->jobsets->find({name => $compare})
or notFound($c, "Jobset $compare doesn't exist."); or notFound($c, "Jobset $compare doesn't exist.");
@ -51,10 +59,17 @@ sub view : Chained('eval') PathPart('') Args(0) {
$c->stash->{otherEval} = $eval2 if defined $eval2; $c->stash->{otherEval} = $eval2 if defined $eval2;
my @builds = $eval->builds->search({}, { order_by => ["job", "system", "id"], columns => [@buildListColumns] }); sub cmpBuilds {
my @builds2 = defined $eval2 my ($a, $b) = @_;
? $eval2->builds->search({}, { order_by => ["job", "system", "id"], columns => [@buildListColumns] }) return $a->get_column('job') cmp $b->get_column('job')
: (); || $a->get_column('system') cmp $b->get_column('system')
}
my @builds = $eval->builds->search($filter, { columns => [@buildListColumns] });
my @builds2 = defined $eval2 ? $eval2->builds->search($filter, { columns => [@buildListColumns] }) : ();
@builds = sort { cmpBuilds($a, $b) } @builds;
@builds2 = sort { cmpBuilds($a, $b) } @builds2;
$c->stash->{stillSucceed} = []; $c->stash->{stillSucceed} = [];
$c->stash->{stillFail} = []; $c->stash->{stillFail} = [];
@ -63,15 +78,19 @@ sub view : Chained('eval') PathPart('') Args(0) {
$c->stash->{new} = []; $c->stash->{new} = [];
$c->stash->{removed} = []; $c->stash->{removed} = [];
$c->stash->{unfinished} = []; $c->stash->{unfinished} = [];
$c->stash->{aborted} = [];
my $n = 0; my $n = 0;
foreach my $build (@builds) { foreach my $build (@builds) {
if ($build->finished != 0 && ($build->buildstatus == 3 || $build->buildstatus == 4)) {
push @{$c->stash->{aborted}}, $build;
next;
}
my $d; my $d;
my $found = 0; my $found = 0;
while ($n < scalar(@builds2)) { while ($n < scalar(@builds2)) {
my $build2 = $builds2[$n]; my $build2 = $builds2[$n];
my $d = $build->get_column('job') cmp $build2->get_column('job') my $d = cmpBuilds($build, $build2);
|| $build->get_column('system') cmp $build2->get_column('system');
last if $d == -1; last if $d == -1;
if ($d == 0) { if ($d == 0) {
$n++; $n++;
@ -135,6 +154,25 @@ sub release : Chained('eval') PathPart('release') Args(0) {
} }
sub cancel : Chained('eval') PathPart('cancel') Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{eval}->project);
my $n = cancelBuilds($c->model('DB')->schema, $c->stash->{eval}->builds);
$c->flash->{successMsg} = "$n builds have been cancelled.";
$c->res->redirect($c->uri_for($c->controller('JobsetEval')->action_for('view'), $c->req->captures));
}
sub restart_aborted : Chained('eval') PathPart('restart-aborted') Args(0) {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{eval}->project);
my $builds = $c->stash->{eval}->builds->search({ finished => 1, buildstatus => { -in => [3, 4] } });
my $n = restartBuilds($c->model('DB')->schema, $builds);
$c->flash->{successMsg} = "$n builds have been restarted.";
$c->res->redirect($c->uri_for($c->controller('JobsetEval')->action_for('view'), $c->req->captures));
}
# Hydra::Base::Controller::NixChannel needs this. # Hydra::Base::Controller::NixChannel needs this.
sub nix : Chained('eval') PathPart('channel') CaptureArgs(0) { sub nix : Chained('eval') PathPart('channel') CaptureArgs(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
@ -144,8 +182,20 @@ sub nix : Chained('eval') PathPart('channel') CaptureArgs(0) {
->search({ finished => 1, buildstatus => 0 }, ->search({ finished => 1, buildstatus => 0 },
{ columns => [@buildListColumns, 'drvpath', 'description', 'homepage'] { columns => [@buildListColumns, 'drvpath', 'description', 'homepage']
, join => ["buildoutputs"] , join => ["buildoutputs"]
, order_by => ["build.id", "buildoutputs.name"]
, '+select' => ['buildoutputs.path', 'buildoutputs.name'], '+as' => ['outpath', 'outname'] }); , '+select' => ['buildoutputs.path', 'buildoutputs.name'], '+as' => ['outpath', 'outname'] });
} }
sub job : Chained('eval') PathPart('job') {
my ($self, $c, $job, @rest) = @_;
my $build = $c->stash->{eval}->builds->find({job => $job});
notFound($c, "This evaluation has no job with the specified name.") unless defined $build;
$c->res->redirect($c->uri_for($c->controller('Build')->action_for("build"), [$build->id], @rest));
}
1; 1;

View file

@ -1,5 +1,6 @@
package Hydra::Controller::Project; package Hydra::Controller::Project;
use utf8;
use strict; use strict;
use warnings; use warnings;
use base 'Hydra::Base::Controller::ListBuilds'; use base 'Hydra::Base::Controller::ListBuilds';
@ -9,35 +10,15 @@ use Hydra::Helper::CatalystUtils;
sub projectChain :Chained('/') :PathPart('project') :CaptureArgs(1) { sub projectChain :Chained('/') :PathPart('project') :CaptureArgs(1) {
my ($self, $c, $projectName) = @_; my ($self, $c, $projectName) = @_;
$c->stash->{params}->{name} //= $projectName;
my $project = $c->model('DB::Projects')->find($projectName, { columns => [ $c->stash->{project} = $c->model('DB::Projects')->find($projectName, {
"me.name", join => [ 'releases' ],
"me.displayName", order_by => { -desc => "releases.timestamp" },
"me.description", });
"me.enabled",
"me.hidden",
"me.homepage",
"owner.username",
"owner.fullname",
"views.name",
"releases.name",
"releases.timestamp",
"jobsets.name",
], join => [ 'owner', 'views', 'releases', 'jobsets' ], order_by => { -desc => "releases.timestamp" }, collapse => 1 });
if ($project) { notFound($c, "Project $projectName doesn't exist.")
$c->stash->{project} = $project; if !$c->stash->{project} && !($c->action->name eq "project" and $c->request->method eq "PUT");
} else {
if ($c->action->name eq "project" and $c->request->method eq "PUT") {
$c->stash->{projectName} = $projectName;
} else {
$self->status_not_found(
$c,
message => "Project $projectName doesn't exist."
);
$c->detach;
}
}
} }
@ -53,55 +34,27 @@ sub project_GET {
$c->stash->{releases} = [$c->stash->{project}->releases->search({}, $c->stash->{releases} = [$c->stash->{project}->releases->search({},
{order_by => ["timestamp DESC"]})]; {order_by => ["timestamp DESC"]})];
$self->status_ok( $self->status_ok($c, entity => $c->stash->{project});
$c,
entity => $c->stash->{project}
);
} }
sub project_PUT { sub project_PUT {
my ($self, $c) = @_; my ($self, $c) = @_;
if (defined $c->stash->{project}) { if (defined $c->stash->{project}) {
error($c, "Cannot rename project `$c->stash->{params}->{oldName}' over existing project `$c->stash->{project}->name") if defined $c->stash->{params}->{oldName};
requireProjectOwner($c, $c->stash->{project}); requireProjectOwner($c, $c->stash->{project});
txn_do($c->model('DB')->schema, sub { txn_do($c->model('DB')->schema, sub {
updateProject($c, $c->stash->{project}); updateProject($c, $c->stash->{project});
}); });
if ($c->req->looks_like_browser) { my $uri = $c->uri_for($self->action_for("project"), [$c->stash->{project}->name]) . "#tabs-configuration";
$c->res->redirect($c->uri_for($self->action_for("project"), [$c->stash->{project}->name]) . "#tabs-configuration"); $self->status_ok($c, entity => { redirect => "$uri" });
} else {
$self->status_no_content($c);
}
} elsif (defined $c->stash->{params}->{oldName}) {
my $project = $c->model('DB::Projects')->find($c->stash->{params}->{oldName});
if (defined $project) {
requireProjectOwner($c, $project);
txn_do($c->model('DB')->schema, sub {
updateProject($c, $project);
});
my $uri = $c->uri_for($self->action_for("project"), [$project->name]); $c->flash->{successMsg} = "The project configuration has been updated.";
}
if ($c->req->looks_like_browser) { else {
$c->res->redirect($uri . "#tabs-configuration");
} else {
$self->status_created(
$c,
location => "$uri",
entity => { name => $project->name, uri => "$uri", type => "project" }
);
}
} else {
$self->status_not_found(
$c,
message => "Project $c->stash->{params}->{oldName} doesn't exist."
);
}
} else {
requireMayCreateProjects($c); requireMayCreateProjects($c);
error($c, "Invalid project name: $c->stash->{projectName}") if $c->stash->{projectName} !~ /^$projectNameRE$/;
my $project; my $project;
txn_do($c->model('DB')->schema, sub { txn_do($c->model('DB')->schema, sub {
@ -110,23 +63,34 @@ sub project_PUT {
# valid. Idem for the owner. # valid. Idem for the owner.
my $owner = $c->user->username; my $owner = $c->user->username;
$project = $c->model('DB::Projects')->create( $project = $c->model('DB::Projects')->create(
{name => $c->stash->{projectName}, displayname => "", owner => $owner}); { name => ".tmp", displayname => "", owner => $owner });
updateProject($c, $project); updateProject($c, $project);
}); });
my $uri = $c->uri_for($self->action_for("project"), [$project->name]); my $uri = $c->uri_for($self->action_for("project"), [$project->name]);
if ($c->req->looks_like_browser) { $self->status_created($c,
$c->res->redirect($uri . "#tabs-configuration");
} else {
$self->status_created(
$c,
location => "$uri", location => "$uri",
entity => { name => $project->name, uri => "$uri", type => "project" } entity => { name => $project->name, uri => "$uri", redirect => "$uri", type => "project" });
);
}
} }
} }
sub project_DELETE {
my ($self, $c) = @_;
requireProjectOwner($c, $c->stash->{project});
txn_do($c->model('DB')->schema, sub {
$c->stash->{project}->jobsetevals->delete;
$c->stash->{project}->builds->delete;
$c->stash->{project}->delete;
});
my $uri = $c->res->redirect($c->uri_for("/"));
$self->status_ok($c, entity => { redirect => "$uri" });
$c->flash->{successMsg} = "The project has been deleted.";
}
sub edit : Chained('projectChain') PathPart Args(0) { sub edit : Chained('projectChain') PathPart Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
@ -138,36 +102,10 @@ sub edit : Chained('projectChain') PathPart Args(0) {
} }
sub submit : Chained('projectChain') PathPart Args(0) {
my ($self, $c) = @_;
requirePost($c);
if (($c->request->params->{submit} // "") eq "delete") {
txn_do($c->model('DB')->schema, sub {
$c->stash->{project}->jobsetevals->delete_all;
$c->stash->{project}->builds->delete_all;
$c->stash->{project}->delete;
});
return $c->res->redirect($c->uri_for("/"));
}
my $newName = trim $c->stash->{params}->{name};
my $oldName = trim $c->stash->{project}->name;
unless ($oldName eq $newName) {
$c->stash->{params}->{oldName} = $oldName;
$c->stash->{projectName} = $newName;
undef $c->stash->{project};
}
project_PUT($self, $c);
}
sub requireMayCreateProjects { sub requireMayCreateProjects {
my ($c) = @_; my ($c) = @_;
requireUser($c);
requireLogin($c) if !$c->user_exists; accessDenied($c, "Only administrators or authorised users can perform this operation.")
error($c, "Only administrators or authorised users can perform this operation.")
unless $c->check_user_roles('admin') || $c->check_user_roles('create-projects'); unless $c->check_user_roles('admin') || $c->check_user_roles('create-projects');
} }
@ -183,15 +121,6 @@ sub create : Path('/create-project') {
} }
sub create_submit : Path('/create-project/submit') {
my ($self, $c) = @_;
$c->stash->{projectName} = trim $c->stash->{params}->{name};
project_PUT($self, $c);
}
sub create_jobset : Chained('projectChain') PathPart('create-jobset') Args(0) { sub create_jobset : Chained('projectChain') PathPart('create-jobset') Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
@ -200,15 +129,7 @@ sub create_jobset : Chained('projectChain') PathPart('create-jobset') Args(0) {
$c->stash->{template} = 'edit-jobset.tt'; $c->stash->{template} = 'edit-jobset.tt';
$c->stash->{create} = 1; $c->stash->{create} = 1;
$c->stash->{edit} = 1; $c->stash->{edit} = 1;
} $c->stash->{totalShares} = getTotalShares($c->model('DB')->schema);
sub create_jobset_submit : Chained('projectChain') PathPart('create-jobset/submit') Args(0) {
my ($self, $c) = @_;
$c->stash->{jobsetName} = trim $c->stash->{params}->{name};
Hydra::Controller::Jobset::jobset_PUT($self, $c);
} }
@ -218,15 +139,18 @@ sub updateProject {
my $owner = $project->owner; my $owner = $project->owner;
if ($c->check_user_roles('admin') and defined $c->stash->{params}->{owner}) { if ($c->check_user_roles('admin') and defined $c->stash->{params}->{owner}) {
$owner = trim $c->stash->{params}->{owner}; $owner = trim $c->stash->{params}->{owner};
error($c, "Invalid owner: $owner") error($c, "The user name $owner does not exist.")
unless defined $c->model('DB::Users')->find({username => $owner}); unless defined $c->model('DB::Users')->find($owner);
} }
my $projectName = $c->stash->{projectName} or $project->name; my $projectName = $c->stash->{params}->{name};
error($c, "Invalid project name: $projectName") if $projectName !~ /^$projectNameRE$/; error($c, "Invalid project identifier $projectName.") if $projectName !~ /^$projectNameRE$/;
error($c, "Cannot rename project to $projectName since that identifier is already taken.")
if $projectName ne $project->name && defined $c->model('DB::Projects')->find($projectName);
my $displayName = trim $c->stash->{params}->{displayname}; my $displayName = trim $c->stash->{params}->{displayname};
error($c, "Invalid display name: $displayName") if $displayName eq ""; error($c, "You must specify a display name.") if $displayName eq "";
$project->update( $project->update(
{ name => $projectName { name => $projectName
@ -244,10 +168,6 @@ sub updateProject {
sub get_builds : Chained('projectChain') PathPart('') CaptureArgs(0) { sub get_builds : Chained('projectChain') PathPart('') CaptureArgs(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
$c->stash->{allBuilds} = $c->stash->{project}->builds; $c->stash->{allBuilds} = $c->stash->{project}->builds;
$c->stash->{jobStatus} = $c->model('DB')->resultset('JobStatusForProject')
->search({}, {bind => [$c->stash->{project}->name]});
$c->stash->{allJobsets} = $c->stash->{project}->jobsets;
$c->stash->{allJobs} = $c->stash->{project}->jobs;
$c->stash->{latestSucceeded} = $c->model('DB')->resultset('LatestSucceededForProject') $c->stash->{latestSucceeded} = $c->model('DB')->resultset('LatestSucceededForProject')
->search({}, {bind => [$c->stash->{project}->name]}); ->search({}, {bind => [$c->stash->{project}->name]});
$c->stash->{channelBaseName} = $c->stash->{project}->name; $c->stash->{channelBaseName} = $c->stash->{project}->name;

View file

@ -38,7 +38,7 @@ sub updateRelease {
, description => trim $c->request->params->{description} , description => trim $c->request->params->{description}
}); });
$release->releasemembers->delete_all; $release->releasemembers->delete;
foreach my $param (keys %{$c->request->params}) { foreach my $param (keys %{$c->request->params}) {
next unless $param =~ /^member-(\d+)-description$/; next unless $param =~ /^member-(\d+)-description$/;
my $buildId = $1; my $buildId = $1;
@ -72,7 +72,7 @@ sub submit : Chained('release') PathPart('submit') Args(0) {
txn_do($c->model('DB')->schema, sub { txn_do($c->model('DB')->schema, sub {
updateRelease($c, $c->stash->{release}); updateRelease($c, $c->stash->{release});
}); });
$c->res->redirect($c->uri_for($self->action_for("project"), $c->res->redirect($c->uri_for($self->action_for("view"),
[$c->stash->{project}->name, $c->stash->{release}->name])); [$c->stash->{project}->name, $c->stash->{release}->name]));
} }
} }

View file

@ -8,6 +8,7 @@ use Hydra::Helper::CatalystUtils;
use Digest::SHA1 qw(sha1_hex); use Digest::SHA1 qw(sha1_hex);
use Nix::Store; use Nix::Store;
use Nix::Config; use Nix::Config;
use Encode;
# Put this controller at top-level. # Put this controller at top-level.
__PACKAGE__->config->{namespace} = ''; __PACKAGE__->config->{namespace} = '';
@ -33,6 +34,7 @@ sub begin :Private {
$c->stash->{inputTypes} = { $c->stash->{inputTypes} = {
'string' => 'String value', 'string' => 'String value',
'boolean' => 'Boolean', 'boolean' => 'Boolean',
'nix' => 'Nix expression',
'build' => 'Build output', 'build' => 'Build output',
'sysbuild' => 'Build output (same system)' 'sysbuild' => 'Build output (same system)'
}; };
@ -54,12 +56,8 @@ sub index :Path :Args(0) {
$c->stash->{template} = 'overview.tt'; $c->stash->{template} = 'overview.tt';
$c->stash->{projects} = [$c->model('DB::Projects')->search(isAdmin($c) ? {} : {hidden => 0}, {order_by => 'name'})]; $c->stash->{projects} = [$c->model('DB::Projects')->search(isAdmin($c) ? {} : {hidden => 0}, {order_by => 'name'})];
$c->stash->{newsItems} = [$c->model('DB::NewsItems')->search({}, { order_by => ['createtime DESC'], rows => 5 })]; $c->stash->{newsItems} = [$c->model('DB::NewsItems')->search({}, { order_by => ['createtime DESC'], rows => 5 })];
$self->status_ok( $self->status_ok($c,
$c, entity => $c->stash->{projects}
entity => [$c->model('DB::Projects')->search(isAdmin($c) ? {} : {hidden => 0}, {
order_by => 'name',
columns => [ 'name', 'displayname' ]
})]
); );
} }
@ -72,8 +70,7 @@ sub queue_GET {
$c->stash->{flashMsg} //= $c->flash->{buildMsg}; $c->stash->{flashMsg} //= $c->flash->{buildMsg};
$self->status_ok( $self->status_ok(
$c, $c,
entity => [$c->model('DB::Builds')->search( entity => [$c->model('DB::Builds')->search({finished => 0}, { order_by => ["priority DESC", "id"]})]
{finished => 0}, { join => ['project'], order_by => ["priority DESC", "id"], columns => [@buildListColumns], '+select' => ['project.enabled'], '+as' => ['enabled'] })]
); );
} }
@ -100,22 +97,7 @@ sub status_GET {
$c, $c,
entity => [ $c->model('DB::BuildSteps')->search( entity => [ $c->model('DB::BuildSteps')->search(
{ 'me.busy' => 1, 'build.finished' => 0, 'build.busy' => 1 }, { 'me.busy' => 1, 'build.finished' => 0, 'build.busy' => 1 },
{ join => { build => [ 'project', 'job', 'jobset' ] }, { order_by => [ 'machine' ], join => [ 'build' ] }
columns => [
'me.machine',
'me.system',
'me.stepnr',
'me.drvpath',
'me.starttime',
'build.id',
{
'build.project.name' => 'project.name',
'build.jobset.name' => 'jobset.name',
'build.job.name' => 'job.name'
}
],
order_by => [ 'machine' ]
}
) ] ) ]
); );
} }
@ -150,11 +132,9 @@ sub machines :Local Args(0) {
sub get_builds : Chained('/') PathPart('') CaptureArgs(0) { sub get_builds : Chained('/') PathPart('') CaptureArgs(0) {
my ($self, $c) = @_; my ($self, $c) = @_;
$c->stash->{allBuilds} = $c->model('DB::Builds'); $c->stash->{allBuilds} = $c->model('DB::Builds');
$c->stash->{jobStatus} = $c->model('DB')->resultset('JobStatus');
$c->stash->{allJobsets} = $c->model('DB::Jobsets');
$c->stash->{allJobs} = $c->model('DB::Jobs');
$c->stash->{latestSucceeded} = $c->model('DB')->resultset('LatestSucceeded'); $c->stash->{latestSucceeded} = $c->model('DB')->resultset('LatestSucceeded');
$c->stash->{channelBaseName} = "everything"; $c->stash->{channelBaseName} = "everything";
$c->stash->{total} = $c->model('DB::NrBuilds')->find('finished')->count;
} }
@ -213,35 +193,32 @@ sub default :Path {
sub end : ActionClass('RenderView') { sub end : ActionClass('RenderView') {
my ($self, $c) = @_; my ($self, $c) = @_;
my @errors = map { encode_utf8($_); } @{$c->error};
if (defined $c->stash->{json}) { if (defined $c->stash->{json}) {
if (scalar @{$c->error}) { if (scalar @errors) {
$c->stash->{json}->{error} = join "\n", @{$c->error}; $c->stash->{json}->{error} = join "\n", @errors;
$c->clear_errors; $c->clear_errors;
} }
$c->forward('View::JSON'); $c->forward('View::JSON');
} }
if (scalar @{$c->error}) { elsif (scalar @{$c->error}) {
$c->stash->{resource} = { errors => "$c->error" }; $c->stash->{resource} = { error => join "\n", @{$c->error} };
$c->stash->{template} = 'error.tt'; $c->stash->{template} = 'error.tt';
$c->stash->{errors} = $c->error; $c->stash->{errors} = [@errors];
$c->response->status(500) if $c->response->status == 200; $c->response->status(500) if $c->response->status == 200;
if ($c->response->status >= 300) { if ($c->response->status >= 300) {
$c->stash->{httpStatus} = $c->stash->{httpStatus} =
$c->response->status . " " . HTTP::Status::status_message($c->response->status); $c->response->status . " " . HTTP::Status::status_message($c->response->status);
} }
$c->clear_errors; $c->clear_errors;
} elsif (defined $c->stash->{resource} and
(ref $c->stash->{resource} eq ref {}) and
defined $c->stash->{resource}->{error}) {
$c->stash->{template} = 'error.tt';
$c->stash->{httpStatus} =
$c->response->status . " " . HTTP::Status::status_message($c->response->status);
} }
$c->forward('serialize'); $c->forward('serialize') if defined $c->stash->{resource};
} }
sub serialize : ActionClass('Serialize') { } sub serialize : ActionClass('Serialize') { }
@ -282,6 +259,7 @@ sub narinfo :LocalRegex('^([a-z0-9]+).narinfo$') :Args(0) {
my $path = queryPathFromHashPart($hash); my $path = queryPathFromHashPart($hash);
if (!$path) { if (!$path) {
$c->response->status(404);
$c->response->content_type('text/plain'); $c->response->content_type('text/plain');
$c->stash->{plain}->{data} = "does not exist\n"; $c->stash->{plain}->{data} = "does not exist\n";
$c->forward('Hydra::View::Plain'); $c->forward('Hydra::View::Plain');

View file

@ -182,15 +182,11 @@ sub currentUser :Path('/current-user') :ActionClass('REST') { }
sub currentUser_GET { sub currentUser_GET {
my ($self, $c) = @_; my ($self, $c) = @_;
requireLogin($c) if !$c->user_exists; requireUser($c);
$self->status_ok( $self->status_ok(
$c, $c,
entity => $c->model('DB::Users')->find({ 'me.username' => $c->user->username}, { entity => $c->model("DB::Users")->find($c->user->username)
columns => [ "me.fullname", "me.emailaddress", "me.username", "userroles.role" ]
, join => [ "userroles" ]
, collapse => 1
})
); );
} }
@ -198,9 +194,9 @@ sub currentUser_GET {
sub user :Chained('/') PathPart('user') CaptureArgs(1) { sub user :Chained('/') PathPart('user') CaptureArgs(1) {
my ($self, $c, $userName) = @_; my ($self, $c, $userName) = @_;
requireLogin($c) if !$c->user_exists; requireUser($c);
error($c, "You do not have permission to edit other users.") accessDenied($c, "You do not have permission to edit other users.")
if $userName ne $c->user->username && !isAdmin($c); if $userName ne $c->user->username && !isAdmin($c);
$c->stash->{user} = $c->model('DB::Users')->find($userName) $c->stash->{user} = $c->model('DB::Users')->find($userName)
@ -287,7 +283,7 @@ sub edit_POST {
} }
if (isAdmin($c)) { if (isAdmin($c)) {
$user->userroles->delete_all; $user->userroles->delete;
$user->userroles->create({ role => $_}) $user->userroles->create({ role => $_})
foreach paramToList($c, "roles"); foreach paramToList($c, "roles");
} }
@ -303,4 +299,19 @@ sub edit_POST {
} }
sub dashboard :Chained('user') :Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'dashboard.tt';
# Get the N most recent builds for each starred job.
$c->stash->{starredJobs} = [];
foreach my $j ($c->stash->{user}->starredjobs->search({}, { order_by => ['project', 'jobset', 'job'] })) {
my @builds = $j->job->builds->search(
{ },
{ rows => 20, order_by => "id desc" });
push $c->stash->{starredJobs}, { job => $j->job, builds => [@builds] };
}
}
1; 1;

View file

@ -41,7 +41,7 @@ sub updateView {
{ name => $viewName { name => $viewName
, description => trim $c->request->params->{description} }); , description => trim $c->request->params->{description} });
$view->viewjobs->delete_all; $view->viewjobs->delete;
foreach my $param (keys %{$c->request->params}) { foreach my $param (keys %{$c->request->params}) {
next unless $param =~ /^job-(\d+)-name$/; next unless $param =~ /^job-(\d+)-name$/;

View file

@ -2,6 +2,7 @@ package Hydra::Helper::AddBuilds;
use strict; use strict;
use feature 'switch'; use feature 'switch';
use utf8;
use XML::Simple; use XML::Simple;
use IPC::Run; use IPC::Run;
use Nix::Store; use Nix::Store;
@ -15,6 +16,7 @@ use File::Path;
use File::Temp; use File::Temp;
use File::Spec; use File::Spec;
use File::Slurp; use File::Slurp;
use Hydra::Helper::PluginHooks;
our @ISA = qw(Exporter); our @ISA = qw(Exporter);
our @EXPORT = qw( our @EXPORT = qw(
@ -86,10 +88,7 @@ sub fetchInputBuild {
{ order_by => "me.id DESC", rows => 1 { order_by => "me.id DESC", rows => 1
, where => \ attrsToSQL($attrs, "me.id") }); , where => \ attrsToSQL($attrs, "me.id") });
if (!defined $prevBuild || !isValidPath(getMainOutput($prevBuild)->path)) { return () if !defined $prevBuild || !isValidPath(getMainOutput($prevBuild)->path);
print STDERR "input `", $name, "': no previous build available\n";
return undef;
}
#print STDERR "input `", $name, "': using build ", $prevBuild->id, "\n"; #print STDERR "input `", $name, "': using build ", $prevBuild->id, "\n";
@ -148,9 +147,8 @@ sub fetchInputSystemBuild {
return @inputs; return @inputs;
} }
sub fetchInput { sub fetchInput {
my ($plugins, $db, $project, $jobset, $name, $type, $value) = @_; my ($plugins, $db, $project, $jobset, $name, $type, $value, $emailresponsible) = @_;
my @inputs; my @inputs;
if ($type eq "build") { if ($type eq "build") {
@ -159,7 +157,7 @@ sub fetchInput {
elsif ($type eq "sysbuild") { elsif ($type eq "sysbuild") {
@inputs = fetchInputSystemBuild($db, $project, $jobset, $name, $value); @inputs = fetchInputSystemBuild($db, $project, $jobset, $name, $value);
} }
elsif ($type eq "string") { elsif ($type eq "string" || $type eq "nix") {
die unless defined $value; die unless defined $value;
@inputs = { value => $value }; @inputs = { value => $value };
} }
@ -170,7 +168,7 @@ sub fetchInput {
else { else {
my $found = 0; my $found = 0;
foreach my $plugin (@{$plugins}) { foreach my $plugin (@{$plugins}) {
@inputs = $plugin->fetchInput($type, $name, $value); @inputs = $plugin->fetchInput($type, $name, $value, $project, $jobset);
if (defined $inputs[0]) { if (defined $inputs[0]) {
$found = 1; $found = 1;
last; last;
@ -179,7 +177,10 @@ sub fetchInput {
die "input `$name' has unknown type `$type'." unless $found; die "input `$name' has unknown type `$type'." unless $found;
} }
$_->{type} = $type foreach @inputs; foreach my $input (@inputs) {
$input->{type} = $type;
$input->{emailresponsible} = $emailresponsible;
}
return @inputs; return @inputs;
} }
@ -243,6 +244,9 @@ sub inputsToArgs {
when ("boolean") { when ("boolean") {
push @res, "--arg", $input, booleanToString($exprType, $alt->{value}); push @res, "--arg", $input, booleanToString($exprType, $alt->{value});
} }
when ("nix") {
push @res, "--arg", $input, $alt->{value};
}
default { default {
push @res, "--arg", $input, buildInputToString($exprType, $alt); push @res, "--arg", $input, buildInputToString($exprType, $alt);
} }
@ -287,17 +291,25 @@ sub evalJobs {
my $validJob = 1; my $validJob = 1;
foreach my $arg (@{$job->{arg}}) { foreach my $arg (@{$job->{arg}}) {
my $input = $inputInfo->{$arg->{name}}->[$arg->{altnr}]; my $input = $inputInfo->{$arg->{name}}->[$arg->{altnr}];
if ($input->{type} eq "sysbuild" && $input->{system} ne $job->{system}) { $validJob = 0 if $input->{type} eq "sysbuild" && $input->{system} ne $job->{system};
$validJob = 0;
}
}
if ($validJob) {
push(@filteredJobs, $job);
} }
push(@filteredJobs, $job) if $validJob;
} }
$jobs->{job} = \@filteredJobs; $jobs->{job} = \@filteredJobs;
return ($jobs, $nixExprInput); my %jobNames;
my $errors;
foreach my $job (@{$jobs->{job}}) {
$jobNames{$job->{jobName}}++;
if ($jobNames{$job->{jobName}} == 2) {
$errors .= "warning: there are multiple jobs named $job->{jobName}; support for this will go away soon!\n\n";
}
}
# Handle utf-8 characters in error messages. No idea why this works.
utf8::decode($_->{msg}) foreach @{$jobs->{error}};
return ($jobs, $nixExprInput, $errors);
} }
@ -389,7 +401,7 @@ sub getPrevJobsetEval {
# Check whether to add the build described by $buildInfo. # Check whether to add the build described by $buildInfo.
sub checkBuild { sub checkBuild {
my ($db, $project, $jobset, $inputInfo, $nixExprInput, $buildInfo, $buildIds, $prevEval, $jobOutPathMap) = @_; my ($db, $jobset, $inputInfo, $nixExprInput, $buildInfo, $buildMap, $prevEval, $jobOutPathMap, $plugins) = @_;
my @outputNames = sort keys %{$buildInfo->{output}}; my @outputNames = sort keys %{$buildInfo->{output}};
die unless scalar @outputNames; die unless scalar @outputNames;
@ -410,9 +422,7 @@ sub checkBuild {
my $build; my $build;
txn_do($db, sub { txn_do($db, sub {
my $job = $jobset->jobs->update_or_create( my $job = $jobset->jobs->update_or_create({ name => $jobName });
{ name => $jobName
});
# Don't add a build that has already been scheduled for this # Don't add a build that has already been scheduled for this
# job, or has been built but is still a "current" build for # job, or has been built but is still a "current" build for
@ -433,19 +443,19 @@ sub checkBuild {
# semantically unnecessary (because they're implied by # semantically unnecessary (because they're implied by
# the eval), but they give a factor 1000 speedup on # the eval), but they give a factor 1000 speedup on
# the Nixpkgs jobset with PostgreSQL. # the Nixpkgs jobset with PostgreSQL.
{ project => $project->name, jobset => $jobset->name, job => $job->name, { project => $jobset->project->name, jobset => $jobset->name, job => $jobName,
name => $firstOutputName, path => $firstOutputPath }, name => $firstOutputName, path => $firstOutputPath },
{ rows => 1, columns => ['id'], join => ['buildoutputs'] }); { rows => 1, columns => ['id'], join => ['buildoutputs'] });
if (defined $prevBuild) { if (defined $prevBuild) {
print STDERR " already scheduled/built as build ", $prevBuild->id, "\n"; print STDERR " already scheduled/built as build ", $prevBuild->id, "\n";
$buildIds->{$prevBuild->id} = 0; $buildMap->{$prevBuild->id} = { id => $prevBuild->id, jobName => $jobName, new => 0, drvPath => $drvPath };
return; return;
} }
} }
# Prevent multiple builds with the same (job, outPath) from # Prevent multiple builds with the same (job, outPath) from
# being added. # being added.
my $prev = $$jobOutPathMap{$job->name . "\t" . $firstOutputPath}; my $prev = $$jobOutPathMap{$jobName . "\t" . $firstOutputPath};
if (defined $prev) { if (defined $prev) {
print STDERR " already scheduled as build ", $prev, "\n"; print STDERR " already scheduled as build ", $prev, "\n";
return; return;
@ -511,12 +521,13 @@ sub checkBuild {
$build->buildoutputs->create({ name => $_, path => $buildInfo->{output}->{$_}->{path} }) $build->buildoutputs->create({ name => $_, path => $buildInfo->{output}->{$_}->{path} })
foreach @outputNames; foreach @outputNames;
$buildIds->{$build->id} = 1; $buildMap->{$build->id} = { id => $build->id, jobName => $jobName, new => 1, drvPath => $drvPath };
$$jobOutPathMap{$job->name . "\t" . $firstOutputPath} = $build->id; $$jobOutPathMap{$jobName . "\t" . $firstOutputPath} = $build->id;
if ($build->iscachedbuild) { if ($build->iscachedbuild) {
print STDERR " marked as cached build ", $build->id, "\n"; print STDERR " marked as cached build ", $build->id, "\n";
addBuildProducts($db, $build); addBuildProducts($db, $build);
notifyBuildFinished($plugins, $build, []);
} else { } else {
print STDERR " added to queue as build ", $build->id, "\n"; print STDERR " added to queue as build ", $build->id, "\n";
} }
@ -545,6 +556,7 @@ sub checkBuild {
, uri => $input->{uri} , uri => $input->{uri}
, revision => $input->{revision} , revision => $input->{revision}
, value => $input->{value} , value => $input->{value}
, emailresponsible => $input->{emailresponsible}
, dependency => $input->{id} , dependency => $input->{id}
, path => $input->{storePath} || "" # !!! temporary hack , path => $input->{storePath} || "" # !!! temporary hack
, sha256hash => $input->{sha256hash} , sha256hash => $input->{sha256hash}
@ -556,29 +568,4 @@ sub checkBuild {
}; };
sub restartBuild { 1;
my ($db, $build) = @_;
txn_do($db, sub {
my @paths;
push @paths, $build->drvpath;
push @paths, $_->drvpath foreach $build->buildsteps;
my $r = `nix-store --clear-failed-paths @paths`;
$build->update(
{ finished => 0
, busy => 0
, locker => ""
, iscachedbuild => 0
});
$build->buildproducts->delete_all;
# Reset the stats for the evals to which this build belongs.
# !!! Should do this in a trigger.
foreach my $m ($build->jobsetevalmembers->all) {
$m->eval->update({nrsucceeded => undef});
}
});
}

View file

@ -15,8 +15,8 @@ use feature qw/switch/;
our @ISA = qw(Exporter); our @ISA = qw(Exporter);
our @EXPORT = qw( our @EXPORT = qw(
getBuild getPreviousBuild getNextBuild getPreviousSuccessfulBuild getBuild getPreviousBuild getNextBuild getPreviousSuccessfulBuild
error notFound error notFound accessDenied
requireLogin requireProjectOwner requireAdmin requirePost isAdmin isProjectOwner forceLogin requireUser requireProjectOwner requireAdmin requirePost isAdmin isProjectOwner
trim trim
getLatestFinishedEval getLatestFinishedEval
sendEmail sendEmail
@ -27,6 +27,7 @@ our @EXPORT = qw(
parseJobsetName parseJobsetName
showJobName showJobName
showStatus showStatus
getResponsibleAuthors
); );
@ -102,6 +103,12 @@ sub notFound {
} }
sub accessDenied {
my ($c, $msg) = @_;
error($c, $msg, 403);
}
sub backToReferer { sub backToReferer {
my ($c) = @_; my ($c) = @_;
$c->response->redirect($c->session->{referer} || $c->uri_for('/')); $c->response->redirect($c->session->{referer} || $c->uri_for('/'));
@ -110,26 +117,33 @@ sub backToReferer {
} }
sub requireLogin { sub forceLogin {
my ($c) = @_; my ($c) = @_;
$c->session->{referer} = $c->request->uri; $c->session->{referer} = $c->request->uri;
error($c, "This page requires you to sign in.", 403); accessDenied($c, "This page requires you to sign in.");
}
sub requireUser {
my ($c) = @_;
forceLogin($c) if !$c->user_exists;
} }
sub isProjectOwner { sub isProjectOwner {
my ($c, $project) = @_; my ($c, $project) = @_;
return
return $c->user_exists && ($c->check_user_roles('admin') || $c->user->username eq $project->owner->username || defined $c->model('DB::ProjectMembers')->find({ project => $project, userName => $c->user->username })); $c->user_exists &&
(isAdmin($c) ||
$c->user->username eq $project->owner->username ||
defined $c->model('DB::ProjectMembers')->find({ project => $project, userName => $c->user->username }));
} }
sub requireProjectOwner { sub requireProjectOwner {
my ($c, $project) = @_; my ($c, $project) = @_;
requireUser($c);
requireLogin($c) if !$c->user_exists; accessDenied($c, "Only the project members or administrators can perform this operation.")
error($c, "Only the project members or administrators can perform this operation.", 403)
unless isProjectOwner($c, $project); unless isProjectOwner($c, $project);
} }
@ -142,8 +156,8 @@ sub isAdmin {
sub requireAdmin { sub requireAdmin {
my ($c) = @_; my ($c) = @_;
requireLogin($c) if !$c->user_exists; requireUser($c);
error($c, "Only administrators can perform this operation.", 403) accessDenied($c, "Only administrators can perform this operation.")
unless isAdmin($c); unless isAdmin($c);
} }
@ -206,12 +220,12 @@ sub paramToList {
# Security checking of filenames. # Security checking of filenames.
Readonly our $pathCompRE => "(?:[A-Za-z0-9-\+\._\$][A-Za-z0-9-\+\._\$]*)"; Readonly our $pathCompRE => "(?:[A-Za-z0-9-\+\._\$][A-Za-z0-9-\+\._\$:]*)";
Readonly our $relPathRE => "(?:$pathCompRE(?:/$pathCompRE)*)"; Readonly our $relPathRE => "(?:$pathCompRE(?:/$pathCompRE)*)";
Readonly our $relNameRE => "(?:[A-Za-z0-9-_][A-Za-z0-9-\._]*)"; Readonly our $relNameRE => "(?:[A-Za-z0-9-_][A-Za-z0-9-\._]*)";
Readonly our $attrNameRE => "(?:[A-Za-z_][A-Za-z0-9-_]*)"; Readonly our $attrNameRE => "(?:[A-Za-z_][A-Za-z0-9-_]*)";
Readonly our $projectNameRE => "(?:[A-Za-z_][A-Za-z0-9-_]*)"; Readonly our $projectNameRE => "(?:[A-Za-z_][A-Za-z0-9-_]*)";
Readonly our $jobsetNameRE => "(?:[A-Za-z_][A-Za-z0-9-_]*)"; Readonly our $jobsetNameRE => "(?:[A-Za-z_][A-Za-z0-9-_\.]*)";
Readonly our $jobNameRE => "(?:$attrNameRE(?:\\.$attrNameRE)*)"; Readonly our $jobNameRE => "(?:$attrNameRE(?:\\.$attrNameRE)*)";
Readonly our $systemRE => "(?:[a-z0-9_]+-[a-z0-9_]+)"; Readonly our $systemRE => "(?:[a-z0-9_]+-[a-z0-9_]+)";
Readonly our $userNameRE => "(?:[a-z][a-z0-9_\.]*)"; Readonly our $userNameRE => "(?:[a-z][a-z0-9_\.]*)";
@ -246,4 +260,42 @@ sub showStatus {
} }
# Determine who broke/fixed the build.
sub getResponsibleAuthors {
my ($build, $plugins) = @_;
my $prevBuild = getPreviousBuild($build);
my $nrCommits = 0;
my %authors;
my @emailable_authors;
if ($prevBuild) {
foreach my $curInput ($build->buildinputs_builds) {
next unless ($curInput->type eq "git" || $curInput->type eq "hg");
my $prevInput = $prevBuild->buildinputs_builds->find({ name => $curInput->name });
next unless defined $prevInput;
next if $curInput->type ne $prevInput->type;
next if $curInput->uri ne $prevInput->uri;
next if $curInput->revision eq $prevInput->revision;
my @commits;
foreach my $plugin (@{$plugins}) {
push @commits, @{$plugin->getCommits($curInput->type, $curInput->uri, $prevInput->revision, $curInput->revision)};
}
foreach my $commit (@commits) {
#print STDERR "$commit->{revision} by $commit->{author}\n";
$authors{$commit->{author}} = $commit->{email};
push @emailable_authors, $commit->{email} if $curInput->emailresponsible;
$nrCommits++;
}
}
}
return (\%authors, $nrCommits, \@emailable_authors);
}
1; 1;

View file

@ -7,6 +7,7 @@ use File::Basename;
use Config::General; use Config::General;
use Hydra::Helper::CatalystUtils; use Hydra::Helper::CatalystUtils;
use Hydra::Model::DB; use Hydra::Model::DB;
use Nix::Store;
our @ISA = qw(Exporter); our @ISA = qw(Exporter);
our @EXPORT = qw( our @EXPORT = qw(
@ -16,11 +17,13 @@ our @EXPORT = qw(
getPrimaryBuildsForView getPrimaryBuildsForView
getPrimaryBuildTotal getPrimaryBuildTotal
getViewResult getLatestSuccessfulViewResult getViewResult getLatestSuccessfulViewResult
jobsetOverview removeAsciiEscapes getDrvLogPath logContents jobsetOverview removeAsciiEscapes getDrvLogPath findLog logContents
getMainOutput getMainOutput
getEvals getMachines getEvals getMachines
pathIsInsidePrefix pathIsInsidePrefix
captureStdoutStderr); captureStdoutStderr run grab
getTotalShares
cancelBuilds restartBuilds);
sub getHydraHome { sub getHydraHome {
@ -42,11 +45,12 @@ sub getHydraConfig {
# doesn't work. # doesn't work.
sub txn_do { sub txn_do {
my ($db, $coderef) = @_; my ($db, $coderef) = @_;
my $res;
while (1) { while (1) {
eval { eval {
$db->txn_do($coderef); $res = $db->txn_do($coderef);
}; };
last if !$@; return $res if !$@;
die $@ unless $@ =~ "database is locked"; die $@ unless $@ =~ "database is locked";
} }
} }
@ -253,21 +257,46 @@ sub getLatestSuccessfulViewResult {
sub getDrvLogPath { sub getDrvLogPath {
my ($drvPath) = @_; my ($drvPath) = @_;
my $base = basename $drvPath; my $base = basename $drvPath;
my $fn = my $bucketed = substr($base, 0, 2) . "/" . substr($base, 2);
($ENV{NIX_LOG_DIR} || "/nix/var/log/nix") . "/drvs/" my $fn = ($ENV{NIX_LOG_DIR} || "/nix/var/log/nix") . "/drvs/";
. substr($base, 0, 2) . "/" for ($fn . $bucketed . ".bz2", $fn . $bucketed, $fn . $base . ".bz2", $fn . $base) {
. substr($base, 2); return $_ if (-f $_);
return $fn if -f $fn; }
$fn .= ".bz2"; return undef;
return $fn if -f $fn; }
# Find the log of the derivation denoted by $drvPath. It it doesn't
# exist, try other derivations that produced its outputs (@outPaths).
sub findLog {
my ($c, $drvPath, @outPaths) = @_;
if (defined $drvPath) {
my $logPath = getDrvLogPath($drvPath);
return $logPath if defined $logPath;
}
return undef if scalar @outPaths == 0;
my @steps = $c->model('DB::BuildSteps')->search(
{ path => { -in => [@outPaths] } },
{ select => ["drvpath"]
, distinct => 1
, join => "buildstepoutputs"
});
foreach my $step (@steps) {
next unless defined $step->drvpath;
my $logPath = getDrvLogPath($step->drvpath);
return $logPath if defined $logPath;
}
return undef; return undef;
} }
sub logContents { sub logContents {
my ($drvPath, $tail) = @_; my ($logPath, $tail) = @_;
my $logPath = getDrvLogPath($drvPath);
die unless defined $logPath;
my $cmd; my $cmd;
if ($logPath =~ /.bz2$/) { if ($logPath =~ /.bz2$/) {
$cmd = "bzip2 -d < $logPath"; $cmd = "bzip2 -d < $logPath";
@ -381,7 +410,7 @@ sub getEvals {
} }
sub getMachines { sub getMachines {
my $machinesConf = $ENV{"NIX_REMOTE_SYSTEMS"} || "/etc/nix.machines"; my $machinesConf = $ENV{"NIX_REMOTE_SYSTEMS"} || "/etc/nix/machines";
# Read the list of machines. # Read the list of machines.
my %machines = (); my %machines = ();
@ -472,4 +501,102 @@ sub captureStdoutStderr {
} }
sub run {
my (%args) = @_;
my $res = { stdout => "", stderr => "" };
my $stdin = "";
eval {
local $SIG{ALRM} = sub { die "timeout\n" }; # NB: \n required
alarm $args{timeout} if defined $args{timeout};
my @x = ($args{cmd}, \$stdin, \$res->{stdout});
push @x, \$res->{stderr} if $args{grabStderr} // 1;
IPC::Run::run(@x,
init => sub { chdir $args{dir} or die "changing to $args{dir}" if defined $args{dir}; });
alarm 0;
};
if ($@) {
die unless $@ eq "timeout\n"; # propagate unexpected errors
$res->{status} = -1;
$res->{stderr} = "timeout\n";
} else {
$res->{status} = $?;
chomp $res->{stdout} if $args{chomp} // 0;
}
return $res;
}
sub grab {
my (%args) = @_;
my $res = run(%args, grabStderr => 0);
die "command `@{$args{cmd}}' failed with exit status $res->{status}" if $res->{status};
return $res->{stdout};
}
sub getTotalShares {
my ($db) = @_;
return $db->resultset('Jobsets')->search(
{ 'project.enabled' => 1, 'me.enabled' => { '!=' => 0 } },
{ join => 'project', select => { sum => 'schedulingshares' }, as => 'sum' })->single->get_column('sum');
}
sub cancelBuilds($$) {
my ($db, $builds) = @_;
return txn_do($db, sub {
$builds = $builds->search({ finished => 0, busy => 0 });
my $n = $builds->count;
my $time = time();
$builds->update(
{ finished => 1,
, iscachedbuild => 0, buildstatus => 4 # = cancelled
, starttime => $time
, stoptime => $time
});
return $n;
});
}
sub restartBuilds($$) {
my ($db, $builds) = @_;
my $n = 0;
txn_do($db, sub {
my @paths;
$builds = $builds->search({ finished => 1 });
foreach my $build ($builds->all) {
next if !isValidPath($build->drvpath);
push @paths, $build->drvpath;
push @paths, $_->drvpath foreach $build->buildsteps;
registerRoot $build->drvpath;
$build->update(
{ finished => 0
, busy => 0
, locker => ""
, iscachedbuild => 0
});
$n++;
# Reset the stats for the evals to which this build belongs.
# !!! Should do this in a trigger.
$build->jobsetevals->update({nrsucceeded => undef});
}
# Clear Nix's negative failure cache.
# FIXME: Add this to the API.
system("nix-store", "--clear-failed-paths", @paths);
});
return $n;
}
1; 1;

View file

@ -0,0 +1,22 @@
package Hydra::Helper::PluginHooks;
use strict;
use Exporter;
our @ISA = qw(Exporter);
our @EXPORT = qw(
notifyBuildFinished);
sub notifyBuildFinished {
my ($plugins, $build, $dependents) = @_;
foreach my $plugin (@{$plugins}) {
eval {
$plugin->buildFinished($build, $dependents);
};
if ($@) {
print STDERR "$plugin->buildFinished: $@\n";
}
}
}
1;

View file

@ -38,7 +38,7 @@ sub supportedInputTypes {
# Called to fetch an input of type $type. $value is the input # Called to fetch an input of type $type. $value is the input
# location, typically the repository URL. # location, typically the repository URL.
sub fetchInput { sub fetchInput {
my ($self, $type, $name, $value) = @_; my ($self, $type, $name, $value, $project, $jobset) = @_;
return undef; return undef;
} }

View file

@ -25,21 +25,8 @@ sub fetchInput {
my $stdout; my $stderr; my $stdout; my $stderr;
my $cacheDir = getSCMCacheDir . "/bzr";
mkpath($cacheDir);
my $clonePath = $cacheDir . "/" . sha256_hex($uri);
if (! -d $clonePath) {
(my $res, $stdout, $stderr) = captureStdoutStderr(600, "bzr", "branch", $uri, $clonePath);
die "error cloning bazaar branch at `$uri':\n$stderr" if $res;
}
chdir $clonePath or die $!;
(my $res, $stdout, $stderr) = captureStdoutStderr(600, "bzr", "pull");
die "error pulling latest change bazaar branch at `$uri':\n$stderr" if $res;
# First figure out the last-modified revision of the URI. # First figure out the last-modified revision of the URI.
my @cmd = (["bzr", "revno"], "|", ["sed", 's/^ *\([0-9]*\).*/\1/']); my @cmd = (["bzr", "revno", $uri], "|", ["sed", 's/^ *\([0-9]*\).*/\1/']);
IPC::Run::run(@cmd, \$stdout, \$stderr); IPC::Run::run(@cmd, \$stdout, \$stderr);
die "cannot get head revision of Bazaar branch at `$uri':\n$stderr" if $?; die "cannot get head revision of Bazaar branch at `$uri':\n$stderr" if $?;
@ -61,7 +48,7 @@ sub fetchInput {
$ENV{"NIX_PREFETCH_BZR_LEAVE_DOT_BZR"} = $type eq "bzr-checkout" ? "1" : "0"; $ENV{"NIX_PREFETCH_BZR_LEAVE_DOT_BZR"} = $type eq "bzr-checkout" ? "1" : "0";
(my $res, $stdout, $stderr) = captureStdoutStderr(600, (my $res, $stdout, $stderr) = captureStdoutStderr(600,
"nix-prefetch-bzr", $clonePath, $revision); "nix-prefetch-bzr", $uri, $revision);
die "cannot check out Bazaar branch `$uri':\n$stderr" if $res; die "cannot check out Bazaar branch `$uri':\n$stderr" if $res;
($sha256, $storePath) = split ' ', $stdout; ($sha256, $storePath) = split ' ', $stdout;

View file

@ -0,0 +1,104 @@
package Hydra::Plugin::DarcsInput;
use strict;
use parent 'Hydra::Plugin';
use Digest::SHA qw(sha256_hex);
use File::Path;
use Hydra::Helper::Nix;
use Nix::Store;
sub supportedInputTypes {
my ($self, $inputTypes) = @_;
$inputTypes->{'darcs'} = 'Darcs checkout';
}
sub fetchInput {
my ($self, $type, $name, $uri) = @_;
return undef if $type ne "darcs";
my $timestamp = time;
my $sha256;
my $storePath;
my $revCount;
my $cacheDir = getSCMCacheDir . "/git";
mkpath($cacheDir);
my $clonePath = $cacheDir . "/" . sha256_hex($uri);
$uri =~ s|^file://||; # darcs wants paths, not file:// uris
my $stdout = ""; my $stderr = ""; my $res;
if (! -d $clonePath) {
# Clone the repository.
$res = run(timeout => 600,
cmd => ["darcs", "get", "--lazy", $uri, $clonePath],
dir => $ENV{"TMPDIR"});
die "Error getting darcs repo at `$uri':\n$stderr" if $res->{status};
}
# Update the repository to match $uri.
($res, $stdout, $stderr) = captureStdoutStderr(600,
("darcs", "pull", "-a", "--repodir", $clonePath, "$uri"));
die "Error fetching latest change from darcs repo at `$uri':\n$stderr" if $res;
($res, $stdout, $stderr) = captureStdoutStderr(600,
("darcs", "changes", "--last", "1", "--xml", "--repodir", $clonePath));
die "Error getting revision ID of darcs repo at `$uri':\n$stderr" if $res;
$stdout =~ /^<patch.*hash='([0-9a-fA-F-]+)'/sm; # sigh.
my $revision = $1;
die "Error obtaining revision from output: $stdout\nstderr = $stderr)" unless $revision =~ /^[0-9a-fA-F-]+$/;
die "Error getting a revision identifier at `$uri':\n$stderr" if $res;
# Some simple caching: don't check a uri/revision more than once.
my $cachedInput ;
($cachedInput) = $self->{db}->resultset('CachedDarcsInputs')->search(
{uri => $uri, revision => $revision},
{rows => 1});
if (defined $cachedInput && isValidPath($cachedInput->storepath)) {
$storePath = $cachedInput->storepath;
$sha256 = $cachedInput->sha256hash;
$revision = $cachedInput->revision;
$revCount = $cachedInput->revcount;
} else {
# Then download this revision into the store.
print STDERR "checking out darcs repo $uri\n";
my $tmpDir = File::Temp->newdir("hydra-darcs-export.XXXXXX", CLEANUP => 1, TMPDIR => 1) or die;
(system "darcs", "get", "--lazy", $clonePath, "$tmpDir/export", "--quiet",
"--to-match", "hash $revision") == 0
or die "darcs export failed";
$revCount = `darcs changes --count --repodir $tmpDir/export`; chomp $revCount;
die "darcs changes --count failed" if $? != 0;
system "rm", "-rf", "$tmpDir/export/_darcs";
$storePath = addToStore("$tmpDir/export", 1, "sha256");
$sha256 = queryPathHash($storePath);
$sha256 =~ s/sha256://;
txn_do($self->{db}, sub {
$self->{db}->resultset('CachedDarcsInputs')->update_or_create(
{ uri => $uri
, revision => $revision
, revcount => $revCount
, sha256hash => $sha256
, storepath => $storePath
});
});
}
$revision =~ /^([0-9]+)/;
my $shortRev = $1;
return
{ uri => $uri
, storePath => $storePath
, sha256hash => $sha256
, revision => $revision
, revCount => int($revCount)
, shortRev => $shortRev
};
}
1;

View file

@ -28,6 +28,10 @@ The following dependent jobs also failed:
* [% showJobName(b) %] ([% baseurl %]/build/[% b.id %]) * [% showJobName(b) %] ([% baseurl %]/build/[% b.id %])
[% END -%] [% END -%]
[% END -%]
[% IF nrCommits > 0 -%]
This is likely due to [% IF nrCommits > 1 -%][% nrCommits %] commits by [% END -%][% authorList %].
[% END -%] [% END -%]
[% IF build.buildstatus == 0 -%] [% IF build.buildstatus == 0 -%]
Yay! Yay!
@ -66,7 +70,7 @@ sub buildFinished {
my $to = $b->jobset->emailoverride ne "" ? $b->jobset->emailoverride : $b->maintainers; my $to = $b->jobset->emailoverride ne "" ? $b->jobset->emailoverride : $b->maintainers;
foreach my $address (split ",", $to) { foreach my $address (split ",", ($to // "")) {
$address = trim $address; $address = trim $address;
$addresses{$address} //= { builds => [] }; $addresses{$address} //= { builds => [] };
@ -74,6 +78,14 @@ sub buildFinished {
} }
} }
my ($authors, $nrCommits, $emailable_authors) = getResponsibleAuthors($build, $self->{plugins});
my $authorList;
if (scalar keys %{$authors} > 0) {
my @x = map { "$_ <$authors->{$_}>" } (sort keys %{$authors});
$authorList = join(" or ", scalar @x > 1 ? join(", ", @x[0..scalar @x - 2]): (), $x[-1]);
$addresses{$_} = { builds => [ $build ] } foreach (@{$emailable_authors});
}
# Send an email to each interested address. # Send an email to each interested address.
# !!! should use the Template Toolkit here. # !!! should use the Template Toolkit here.
@ -89,6 +101,8 @@ sub buildFinished {
, baseurl => $self->{config}->{'base_uri'} || "http://localhost:3000" , baseurl => $self->{config}->{'base_uri'} || "http://localhost:3000"
, showJobName => \&showJobName, showStatus => \&showStatus , showJobName => \&showJobName, showStatus => \&showStatus
, showSystem => index($build->job->name, $build->system) == -1 , showSystem => index($build->job->name, $build->system) == -1
, nrCommits => $nrCommits
, authorList => $authorList
}; };
my $body; my $body;

View file

@ -20,39 +20,34 @@ sub _cloneRepo {
mkpath($cacheDir); mkpath($cacheDir);
my $clonePath = $cacheDir . "/" . sha256_hex($uri); my $clonePath = $cacheDir . "/" . sha256_hex($uri);
my $stdout = ""; my $stderr = ""; my $res; my $res;
if (! -d $clonePath) { if (! -d $clonePath) {
# Clone everything and fetch the branch. # Clone everything and fetch the branch.
# TODO: Optimize the first clone by using "git init $clonePath" and "git remote add origin $uri". # TODO: Optimize the first clone by using "git init $clonePath" and "git remote add origin $uri".
($res, $stdout, $stderr) = captureStdoutStderr(600, "git", "clone", "--branch", $branch, $uri, $clonePath); $res = run(cmd => ["git", "clone", "--branch", $branch, $uri, $clonePath], timeout => 600);
die "error cloning git repo at `$uri':\n$stderr" if $res; die "error cloning git repo at `$uri':\n$res->{stderr}" if $res->{status};
} }
chdir $clonePath or die $!; # !!! urgh, shouldn't do a chdir
# This command forces the update of the local branch to be in the same as # This command forces the update of the local branch to be in the same as
# the remote branch for whatever the repository state is. This command mirrors # the remote branch for whatever the repository state is. This command mirrors
# only one branch of the remote repository. # only one branch of the remote repository.
($res, $stdout, $stderr) = captureStdoutStderr(600, $res = run(cmd => ["git", "fetch", "-fu", "origin", "+$branch:$branch"], dir => $clonePath, timeout => 600);
"git", "fetch", "-fu", "origin", "+$branch:$branch"); $res = run(cmd => ["git", "fetch", "-fu", "origin"], dir => $clonePath, timeout => 600) if $res->{status};
($res, $stdout, $stderr) = captureStdoutStderr(600, die "error fetching latest change from git repo at `$uri':\n$res->{stderr}" if $res->{status};
"git", "fetch", "-fu", "origin") if $res;
die "error fetching latest change from git repo at `$uri':\n$stderr" if $res;
# If deepClone is defined, then we look at the content of the repository # If deepClone is defined, then we look at the content of the repository
# to determine if this is a top-git branch. # to determine if this is a top-git branch.
if (defined $deepClone) { if (defined $deepClone) {
# Checkout the branch to look at its content. # Checkout the branch to look at its content.
($res, $stdout, $stderr) = captureStdoutStderr(600, "git", "checkout", "$branch"); $res = run(cmd => ["git", "checkout", "$branch"], dir => $clonePath);
die "error checking out Git branch '$branch' at `$uri':\n$stderr" if $res; die "error checking out Git branch '$branch' at `$uri':\n$res->{stderr}" if $res->{status};
if (-f ".topdeps") { if (-f ".topdeps") {
# This is a TopGit branch. Fetch all the topic branches so # This is a TopGit branch. Fetch all the topic branches so
# that builders can run "tg patch" and similar. # that builders can run "tg patch" and similar.
($res, $stdout, $stderr) = captureStdoutStderr(600, $res = run(cmd => ["tg", "remote", "--populate", "origin"], dir => $clonePath, timeout => 600);
"tg", "remote", "--populate", "origin"); print STDERR "warning: `tg remote --populate origin' failed:\n$res->{stderr}" if $res->{status};
print STDERR "warning: `tg remote --populate origin' failed:\n$stderr" if $res;
} }
} }
@ -64,7 +59,6 @@ sub _parseValue {
(my $uri, my $branch, my $deepClone) = split ' ', $value; (my $uri, my $branch, my $deepClone) = split ' ', $value;
$branch = defined $branch ? $branch : "master"; $branch = defined $branch ? $branch : "master";
return ($uri, $branch, $deepClone); return ($uri, $branch, $deepClone);
} }
sub fetchInput { sub fetchInput {
@ -80,19 +74,13 @@ sub fetchInput {
my $sha256; my $sha256;
my $storePath; my $storePath;
my ($res, $stdout, $stderr) = captureStdoutStderr(600, my $revision = grab(cmd => ["git", "rev-parse", "$branch"], dir => $clonePath, chomp => 1);
("git", "rev-parse", "$branch")); die "did not get a well-formated revision number of Git branch '$branch' at `$uri'"
die "error getting revision number of Git branch '$branch' at `$uri':\n$stderr" if $res;
my ($revision) = split /\n/, $stdout;
die "error getting a well-formated revision number of Git branch '$branch' at `$uri':\n$stdout"
unless $revision =~ /^[0-9a-fA-F]+$/; unless $revision =~ /^[0-9a-fA-F]+$/;
my $ref = "refs/heads/$branch";
# Some simple caching: don't check a uri/branch/revision more than once. # Some simple caching: don't check a uri/branch/revision more than once.
# TODO: Fix case where the branch is reset to a previous commit. # TODO: Fix case where the branch is reset to a previous commit.
my $cachedInput ; my $cachedInput;
($cachedInput) = $self->{db}->resultset('CachedGitInputs')->search( ($cachedInput) = $self->{db}->resultset('CachedGitInputs')->search(
{uri => $uri, branch => $branch, revision => $revision}, {uri => $uri, branch => $branch, revision => $revision},
{rows => 1}); {rows => 1});
@ -123,10 +111,7 @@ sub fetchInput {
$ENV{"NIX_PREFETCH_GIT_DEEP_CLONE"} = "1"; $ENV{"NIX_PREFETCH_GIT_DEEP_CLONE"} = "1";
} }
($res, $stdout, $stderr) = captureStdoutStderr(600, "nix-prefetch-git", $clonePath, $revision); ($sha256, $storePath) = split ' ', grab(cmd => ["nix-prefetch-git", $clonePath, $revision], chomp => 1);
die "cannot check out Git repository branch '$branch' at `$uri':\n$stderr" if $res;
($sha256, $storePath) = split ' ', $stdout;
txn_do($self->{db}, sub { txn_do($self->{db}, sub {
$self->{db}->resultset('CachedGitInputs')->update_or_create( $self->{db}->resultset('CachedGitInputs')->update_or_create(
@ -143,12 +128,9 @@ sub fetchInput {
# number of commits in the history of this revision (revCount) # number of commits in the history of this revision (revCount)
# the output of git-describe (gitTag), and the abbreviated # the output of git-describe (gitTag), and the abbreviated
# revision (shortRev). # revision (shortRev).
my $revCount = `git rev-list $revision | wc -l`; chomp $revCount; my $revCount = scalar(split '\n', grab(cmd => ["git", "rev-list", "$revision"], dir => $clonePath));
die "git rev-list failed" if $? != 0; my $gitTag = grab(cmd => ["git", "describe", "--always", "$revision"], dir => $clonePath, chomp => 1);
my $gitTag = `git describe --always $revision`; chomp $gitTag; my $shortRev = grab(cmd => ["git", "rev-parse", "--short", "$revision"], dir => $clonePath, chomp => 1);
die "git describe failed" if $? != 0;
my $shortRev = `git rev-parse --short $revision`; chomp $shortRev;
die "git rev-parse failed" if $? != 0;
return return
{ uri => $uri { uri => $uri
@ -172,9 +154,7 @@ sub getCommits {
my $clonePath = $self->_cloneRepo($uri, $branch, $deepClone); my $clonePath = $self->_cloneRepo($uri, $branch, $deepClone);
my $out; my $out = grab(cmd => ["git", "log", "--pretty=format:%H%x09%an%x09%ae%x09%at", "$rev1..$rev2"], dir => $clonePath);
IPC::Run::run(["git", "log", "--pretty=format:%H%x09%an%x09%ae%x09%at", "$rev1..$rev2"], \undef, \$out)
or die "cannot get git logs: $?";
my $res = []; my $res = [];
foreach my $line (split /\n/, $out) { foreach my $line (split /\n/, $out) {

View file

@ -9,7 +9,7 @@ sub buildFinished {
my ($self, $build, $dependents) = @_; my ($self, $build, $dependents) = @_;
my $cfg = $self->{config}->{hipchat}; my $cfg = $self->{config}->{hipchat};
my @config = ref $cfg eq "ARRAY" ? @$cfg : ($cfg); my @config = defined $cfg ? ref $cfg eq "ARRAY" ? @$cfg : ($cfg) : ();
my $baseurl = $self->{config}->{'base_uri'} || "http://localhost:3000"; my $baseurl = $self->{config}->{'base_uri'} || "http://localhost:3000";
@ -37,33 +37,7 @@ sub buildFinished {
return if scalar keys %rooms == 0; return if scalar keys %rooms == 0;
# Determine who broke/fixed the build. my ($authors, $nrCommits) = getResponsibleAuthors($build, $self->{plugins});
my $prevBuild = getPreviousBuild($build);
my $nrCommits = 0;
my %authors;
if ($prevBuild) {
foreach my $curInput ($build->buildinputs_builds) {
next unless $curInput->type eq "git";
my $prevInput = $prevBuild->buildinputs_builds->find({ name => $curInput->name });
next unless defined $prevInput;
next if $curInput->type ne $prevInput->type;
next if $curInput->uri ne $prevInput->uri;
my @commits;
foreach my $plugin (@{$self->{plugins}}) {
push @commits, @{$plugin->getCommits($curInput->type, $curInput->uri, $prevInput->revision, $curInput->revision)};
}
foreach my $commit (@commits) {
print STDERR "$commit->{revision} by $commit->{author}\n";
$authors{$commit->{author}} = $commit->{email};
$nrCommits++;
}
}
}
# Send a message to each room. # Send a message to each room.
foreach my $roomId (keys %rooms) { foreach my $roomId (keys %rooms) {
@ -83,16 +57,15 @@ sub buildFinished {
$msg .= " (and ${\scalar @deps} others)" if scalar @deps > 0; $msg .= " (and ${\scalar @deps} others)" if scalar @deps > 0;
$msg .= ": <a href='$baseurl/build/${\$build->id}'>" . showStatus($build) . "</a>"; $msg .= ": <a href='$baseurl/build/${\$build->id}'>" . showStatus($build) . "</a>";
if (scalar keys %authors > 0) { if (scalar keys %{$authors} > 0) {
# FIXME: HTML escaping # FIXME: HTML escaping
my @x = map { "<a href='mailto:$authors{$_}'>$_</a>" } (sort keys %authors); my @x = map { "<a href='mailto:$authors->{$_}'>$_</a>" } (sort keys %{$authors});
$msg .= ", likely due to "; $msg .= ", likely due to ";
$msg .= "$nrCommits commits by " if $nrCommits > 1; $msg .= "$nrCommits commits by " if $nrCommits > 1;
$msg .= join(" or ", scalar @x > 1 ? join(", ", @x[0..scalar @x - 2]) : (), $x[-1]); $msg .= join(" or ", scalar @x > 1 ? join(", ", @x[0..scalar @x - 2]) : (), $x[-1]);
} }
print STDERR "sending hipchat notification to room $roomId: $msg\n"; print STDERR "sending hipchat notification to room $roomId: $msg\n";
next;
my $ua = LWP::UserAgent->new(); my $ua = LWP::UserAgent->new();
my $resp = $ua->post('https://api.hipchat.com/v1/rooms/message?format=json&auth_token=' . $room->{room}->{token}, { my $resp = $ua->post('https://api.hipchat.com/v1/rooms/message?format=json&auth_token=' . $room->{room}->{token}, {

View file

@ -12,21 +12,33 @@ sub supportedInputTypes {
$inputTypes->{'hg'} = 'Mercurial checkout'; $inputTypes->{'hg'} = 'Mercurial checkout';
} }
sub _parseValue {
my ($value) = @_;
(my $uri, my $id) = split ' ', $value;
$id = defined $id ? $id : "default";
return ($uri, $id);
}
sub _clonePath {
my ($uri) = @_;
my $cacheDir = getSCMCacheDir . "/hg";
mkpath($cacheDir);
return $cacheDir . "/" . sha256_hex($uri);
}
sub fetchInput { sub fetchInput {
my ($self, $type, $name, $value) = @_; my ($self, $type, $name, $value) = @_;
return undef if $type ne "hg"; return undef if $type ne "hg";
(my $uri, my $id) = split ' ', $value; (my $uri, my $id) = _parseValue($value);
$id = defined $id ? $id : "default"; $id = defined $id ? $id : "default";
# init local hg clone # init local hg clone
my $stdout = ""; my $stderr = ""; my $stdout = ""; my $stderr = "";
my $cacheDir = getSCMCacheDir . "/hg"; my $clonePath = _clonePath($uri);
mkpath($cacheDir);
my $clonePath = $cacheDir . "/" . sha256_hex($uri);
if (! -d $clonePath) { if (! -d $clonePath) {
(my $res, $stdout, $stderr) = captureStdoutStderr(600, (my $res, $stdout, $stderr) = captureStdoutStderr(600,
@ -85,4 +97,32 @@ sub fetchInput {
}; };
} }
sub getCommits {
my ($self, $type, $value, $rev1, $rev2) = @_;
return [] if $type ne "hg";
return [] unless $rev1 =~ /^[0-9a-f]+$/;
return [] unless $rev2 =~ /^[0-9a-f]+$/;
my ($uri, $id) = _parseValue($value);
my $clonePath = _clonePath($uri);
chdir $clonePath or die $!;
my $out;
IPC::Run::run(["hg", "log", "--template", "{node|short}\t{author|person}\t{author|email}\n", "-r", "$rev1:$rev2", $clonePath], \undef, \$out)
or die "cannot get mercurial logs: $?";
my $res = [];
foreach my $line (split /\n/, $out) {
if ($line ne "") {
my ($revision, $author, $email) = split "\t", $line;
push @$res, { revision => $revision, author => $author, email => $email };
}
}
return $res;
}
1; 1;

View file

@ -34,8 +34,13 @@ sub fetchInput {
} else { } else {
print STDERR "copying input ", $name, " from $uri\n"; print STDERR "copying input ", $name, " from $uri\n";
if ( $uri =~ /^\// ) {
$storePath = `nix-store --add "$uri"` $storePath = `nix-store --add "$uri"`
or die "cannot copy path $uri to the Nix store.\n"; or die "cannot copy path $uri to the Nix store.\n";
} else {
$storePath = `PRINT_PATH=1 nix-prefetch-url "$uri" | tail -n 1`
or die "cannot fetch $uri to the Nix store.\n";
}
chomp $storePath; chomp $storePath;
$sha256 = (queryPathInfo($storePath, 0))[1] or die; $sha256 = (queryPathInfo($storePath, 0))[1] or die;

View file

@ -0,0 +1,149 @@
package Hydra::Plugin::S3Backup;
use strict;
use parent 'Hydra::Plugin';
use File::Temp;
use File::Basename;
use Fcntl;
use IO::File;
use Net::Amazon::S3;
use Net::Amazon::S3::Client;
use Digest::SHA;
use Nix::Config;
use Nix::Store;
use Hydra::Model::DB;
use Hydra::Helper::CatalystUtils;
my $client;
my %compressors = (
xz => "| $Nix::Config::xz",
bzip2 => "| $Nix::Config::bzip2",
none => ""
);
my $lockfile = Hydra::Model::DB::getHydraPath . "/.hydra-s3backup.lock";
sub buildFinished {
my ($self, $build, $dependents) = @_;
return unless $build->buildstatus == 0 or $build->buildstatus == 6;
my $jobName = showJobName $build;
my $job = $build->job;
my $cfg = $self->{config}->{s3backup};
my @config = defined $cfg ? ref $cfg eq "ARRAY" ? @$cfg : ($cfg) : ();
my @matching_configs = ();
foreach my $bucket_config (@config) {
push @matching_configs, $bucket_config if $jobName =~ /^$bucket_config->{jobs}$/;
}
return unless @matching_configs;
unless (defined $client) {
$client = Net::Amazon::S3::Client->new( s3 => Net::Amazon::S3->new( retry => 1 ) );
}
# !!! Maybe should do per-bucket locking?
my $lockhandle = IO::File->new;
open($lockhandle, "+>", $lockfile) or die "Opening $lockfile: $!";
flock($lockhandle, Fcntl::LOCK_SH) or die "Read-locking $lockfile: $!";
my @needed_paths = ();
foreach my $output ($build->buildoutputs) {
push @needed_paths, $output->path;
}
my %narinfos = ();
my %compression_types = ();
foreach my $bucket_config (@matching_configs) {
my $compression_type =
exists $bucket_config->{compression_type} ? $bucket_config->{compression_type} : "bzip2";
die "Unsupported compression type $compression_type" unless exists $compressors{$compression_type};
if (exists $compression_types{$compression_type}) {
push @{$compression_types{$compression_type}}, $bucket_config;
} else {
$compression_types{$compression_type} = [ $bucket_config ];
$narinfos{$compression_type} = [];
}
}
my $build_id = $build->id;
my $tempdir = File::Temp->newdir("s3-backup-nars-$build_id" . "XXXXX");
my %seen = ();
# Upload nars and build narinfos
while (@needed_paths) {
my $path = shift @needed_paths;
next if exists $seen{$path};
$seen{$path} = undef;
my $hash = substr basename($path), 0, 32;
my ($deriver, $narHash, $time, $narSize, $refs) = queryPathInfo($path, 0);
my $system;
if (defined $deriver and isValidPath($deriver)) {
$system = derivationFromPath($deriver)->{platform};
}
foreach my $reference (@{$refs}) {
push @needed_paths, $reference;
}
while (my ($compression_type, $configs) = each %compression_types) {
my @incomplete_buckets = ();
# Don't do any work if all the buckets have this path
foreach my $bucket_config (@{$configs}) {
my $bucket = $client->bucket( name => $bucket_config->{name} );
my $prefix = exists $bucket_config->{prefix} ? $bucket_config->{prefix} : "";
push @incomplete_buckets, $bucket_config
unless $bucket->object( key => $prefix . "$hash.narinfo" )->exists;
}
next unless @incomplete_buckets;
my $compressor = $compressors{$compression_type};
system("$Nix::Config::binDir/nix-store --export $path $compressor > $tempdir/nar") == 0 or die;
my $digest = Digest::SHA->new(256);
$digest->addfile("$tempdir/nar");
my $file_hash = $digest->hexdigest;
my @stats = stat "$tempdir/nar" or die "Couldn't stat $tempdir/nar";
my $file_size = $stats[7];
my $narinfo = "";
$narinfo .= "StorePath: $path\n";
$narinfo .= "URL: $hash.nar\n";
$narinfo .= "Compression: $compression_type\n";
$narinfo .= "FileHash: sha256:$file_hash\n";
$narinfo .= "FileSize: $file_size\n";
$narinfo .= "NarHash: $narHash\n";
$narinfo .= "NarSize: $narSize\n";
$narinfo .= "References: " . join(" ", map { basename $_ } @{$refs}) . "\n";
if (defined $deriver) {
$narinfo .= "Deriver: " . basename $deriver . "\n";
if (defined $system) {
$narinfo .= "System: $system\n";
}
}
push @{$narinfos{$compression_type}}, { hash => $hash, info => $narinfo };
foreach my $bucket_config (@incomplete_buckets) {
my $bucket = $client->bucket( name => $bucket_config->{name} );
my $prefix = exists $bucket_config->{prefix} ? $bucket_config->{prefix} : "";
my $nar_object = $bucket->object(
key => $prefix . "$hash.nar",
content_type => "application/x-nix-archive"
);
$nar_object->put_filename("$tempdir/nar");
}
}
}
# Upload narinfos
while (my ($compression_type, $infos) = each %narinfos) {
foreach my $bucket_config (@{$compression_types{$compression_type}}) {
foreach my $info (@{$infos}) {
my $bucket = $client->bucket( name => $bucket_config->{name} );
my $prefix = exists $bucket_config->{prefix} ? $bucket_config->{prefix} : "";
my $narinfo_object = $bucket->object(
key => $prefix . $info->{hash} . ".narinfo",
content_type => "text/x-nix-narinfo"
);
$narinfo_object->put($info->{info}) unless $narinfo_object->exists;
}
}
}
}
1;

View file

@ -0,0 +1,111 @@
use utf8;
package Hydra::Schema::AggregateConstituents;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Hydra::Schema::AggregateConstituents
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<AggregateConstituents>
=cut
__PACKAGE__->table("AggregateConstituents");
=head1 ACCESSORS
=head2 aggregate
data_type: 'integer'
is_foreign_key: 1
is_nullable: 0
=head2 constituent
data_type: 'integer'
is_foreign_key: 1
is_nullable: 0
=cut
__PACKAGE__->add_columns(
"aggregate",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
"constituent",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
);
=head1 PRIMARY KEY
=over 4
=item * L</aggregate>
=item * L</constituent>
=back
=cut
__PACKAGE__->set_primary_key("aggregate", "constituent");
=head1 RELATIONS
=head2 aggregate
Type: belongs_to
Related object: L<Hydra::Schema::Builds>
=cut
__PACKAGE__->belongs_to(
"aggregate",
"Hydra::Schema::Builds",
{ id => "aggregate" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
);
=head2 constituent
Type: belongs_to
Related object: L<Hydra::Schema::Builds>
=cut
__PACKAGE__->belongs_to(
"constituent",
"Hydra::Schema::Builds",
{ id => "constituent" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-08-15 00:20:01
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:TLNenyPLIWw2gWsOVhplZw
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -72,6 +72,12 @@ __PACKAGE__->table("BuildInputs");
data_type: 'text' data_type: 'text'
is_nullable: 1 is_nullable: 1
=head2 emailresponsible
data_type: 'integer'
default_value: 0
is_nullable: 0
=head2 dependency =head2 dependency
data_type: 'integer' data_type: 'integer'
@ -105,6 +111,8 @@ __PACKAGE__->add_columns(
{ data_type => "text", is_nullable => 1 }, { data_type => "text", is_nullable => 1 },
"value", "value",
{ data_type => "text", is_nullable => 1 }, { data_type => "text", is_nullable => 1 },
"emailresponsible",
{ data_type => "integer", default_value => 0, is_nullable => 0 },
"dependency", "dependency",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, { data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"path", "path",
@ -168,7 +176,7 @@ __PACKAGE__->belongs_to(
); );
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-08 13:08:15
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tKZAybbNaRIMs9n5tHkqPw # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OaJPzRM+8XGsu3eIkqeYEw
1; 1;

View file

@ -97,6 +97,14 @@ __PACKAGE__->belongs_to(
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:V8MbzKvZNEaeHBJV67+ZMQ # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:V8MbzKvZNEaeHBJV67+ZMQ
my %hint = (
columns => [
'path'
],
);
sub json_hint {
return \%hint;
}
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1; 1;

View file

@ -169,4 +169,21 @@ __PACKAGE__->has_many(
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OZsXJniZ/7EB2iSz7p5y4A # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OZsXJniZ/7EB2iSz7p5y4A
my %hint = (
columns => [
"machine",
"system",
"stepnr",
"drvpath",
"starttime",
],
eager_relations => {
build => 'id'
}
);
sub json_hint {
return \%hint;
}
1; 1;

View file

@ -288,6 +288,36 @@ __PACKAGE__->set_primary_key("id");
=head1 RELATIONS =head1 RELATIONS
=head2 aggregateconstituents_aggregates
Type: has_many
Related object: L<Hydra::Schema::AggregateConstituents>
=cut
__PACKAGE__->has_many(
"aggregateconstituents_aggregates",
"Hydra::Schema::AggregateConstituents",
{ "foreign.aggregate" => "self.id" },
undef,
);
=head2 aggregateconstituents_constituents
Type: has_many
Related object: L<Hydra::Schema::AggregateConstituents>
=cut
__PACKAGE__->has_many(
"aggregateconstituents_constituents",
"Hydra::Schema::AggregateConstituents",
{ "foreign.constituent" => "self.id" },
undef,
);
=head2 buildinputs_builds =head2 buildinputs_builds
Type: has_many Type: has_many
@ -468,9 +498,37 @@ __PACKAGE__->has_many(
undef, undef,
); );
=head2 aggregates
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 Type: many_to_many
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:isCEXACY/PwkvgKHcXvAIg
Composing rels: L</aggregateconstituents_constituents> -> aggregate
=cut
__PACKAGE__->many_to_many(
"aggregates",
"aggregateconstituents_constituents",
"aggregate",
);
=head2 constituents
Type: many_to_many
Composing rels: L</aggregateconstituents_constituents> -> constituent
=cut
__PACKAGE__->many_to_many(
"constituents",
"aggregateconstituents_constituents",
"constituent",
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-08-15 00:20:01
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:U1j/qm0vslb6Jvgu5mGMtw
__PACKAGE__->has_many( __PACKAGE__->has_many(
"dependents", "dependents",
@ -502,6 +560,8 @@ __PACKAGE__->has_many(
__PACKAGE__->many_to_many("jobsetevals", "jobsetevalmembers", "eval"); __PACKAGE__->many_to_many("jobsetevals", "jobsetevalmembers", "eval");
__PACKAGE__->many_to_many("constituents_", "aggregateconstituents_aggregates", "constituent");
sub makeSource { sub makeSource {
my ($name, $query) = @_; my ($name, $query) = @_;
my $source = __PACKAGE__->result_source_instance(); my $source = __PACKAGE__->result_source_instance();
@ -516,36 +576,6 @@ sub makeQueries {
my $activeJobs = "(select distinct project, jobset, job, system from Builds where isCurrent = 1 $constraint)"; my $activeJobs = "(select distinct project, jobset, job, system from Builds where isCurrent = 1 $constraint)";
makeSource(
"JobStatus$name",
# Urgh, can't use "*" in the "select" here because of the status change join.
<<QUERY
select x.*, b.id as statusChangeId, b.timestamp as statusChangeTime
from
(select
(select max(b.id) from Builds b
where
project = activeJobs.project and jobset = activeJobs.jobset
and job = activeJobs.job and system = activeJobs.system
and finished = 1
) as id
from $activeJobs as activeJobs
) as latest
join Builds x using (id)
left join Builds b on
b.id =
(select max(c.id) from Builds c
where
c.finished = 1 and
x.project = c.project and x.jobset = c.jobset and x.job = c.job and x.system = c.system and
x.id > c.id and
((x.buildStatus = 0 and c.buildStatus != 0) or
(x.buildStatus != 0 and c.buildStatus = 0)))
QUERY
);
makeSource("ActiveJobs$name", "select distinct project, jobset, job from Builds where isCurrent = 1 $constraint");
makeSource( makeSource(
"LatestSucceeded$name", "LatestSucceeded$name",
<<QUERY <<QUERY
@ -571,4 +601,30 @@ makeQueries('ForJobset', "and project = ? and jobset = ?");
makeQueries('ForJob', "and project = ? and jobset = ? and job = ?"); makeQueries('ForJob', "and project = ? and jobset = ? and job = ?");
my %hint = (
columns => [
'id',
'finished',
'timestamp',
'starttime',
'stoptime',
'project',
'jobset',
'job',
'nixname',
'system',
'priority',
'busy',
'buildstatus',
'releasename'
],
eager_relations => {
buildoutputs => 'name'
}
);
sub json_hint {
return \%hint;
}
1; 1;

View file

@ -0,0 +1,98 @@
use utf8;
package Hydra::Schema::CachedDarcsInputs;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Hydra::Schema::CachedDarcsInputs
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<CachedDarcsInputs>
=cut
__PACKAGE__->table("CachedDarcsInputs");
=head1 ACCESSORS
=head2 uri
data_type: 'text'
is_nullable: 0
=head2 revision
data_type: 'text'
is_nullable: 0
=head2 sha256hash
data_type: 'text'
is_nullable: 0
=head2 storepath
data_type: 'text'
is_nullable: 0
=head2 revcount
data_type: 'integer'
is_nullable: 0
=cut
__PACKAGE__->add_columns(
"uri",
{ data_type => "text", is_nullable => 0 },
"revision",
{ data_type => "text", is_nullable => 0 },
"sha256hash",
{ data_type => "text", is_nullable => 0 },
"storepath",
{ data_type => "text", is_nullable => 0 },
"revcount",
{ data_type => "integer", is_nullable => 0 },
);
=head1 PRIMARY KEY
=over 4
=item * L</uri>
=item * L</revision>
=back
=cut
__PACKAGE__->set_primary_key("uri", "revision");
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-09-20 11:08:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Yl1slt3SAizijgu0KUTn0A
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -137,8 +137,27 @@ __PACKAGE__->belongs_to(
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" }, { is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
); );
=head2 starredjobs
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 Type: has_many
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t2CCfUjFEz/lO4szROz1AQ
Related object: L<Hydra::Schema::StarredJobs>
=cut
__PACKAGE__->has_many(
"starredjobs",
"Hydra::Schema::StarredJobs",
{
"foreign.job" => "self.name",
"foreign.jobset" => "self.jobset",
"foreign.project" => "self.project",
},
undef,
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:uYKWjewvKBEAuK53u7vKuw
1; 1;

View file

@ -169,6 +169,16 @@ __PACKAGE__->belongs_to(
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:1Dp8B58leBLh4GK0GPw2zg # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:1Dp8B58leBLh4GK0GPw2zg
my %hint = (
columns => [
"revision",
"type",
"uri"
],
);
sub json_hint {
return \%hint;
}
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1; 1;

View file

@ -199,4 +199,22 @@ __PACKAGE__->has_many(
__PACKAGE__->many_to_many(builds => 'buildIds', 'build'); __PACKAGE__->many_to_many(builds => 'buildIds', 'build');
my %hint = (
columns => [
"hasnewbuilds",
"id"
],
relations => {
"builds" => "id"
},
eager_relations => {
# altnr? Does anyone care?
jobsetevalinputs => "name"
}
);
sub json_hint {
return \%hint;
}
1; 1;

View file

@ -57,6 +57,12 @@ __PACKAGE__->table("JobsetInputs");
data_type: 'text' data_type: 'text'
is_nullable: 0 is_nullable: 0
=head2 emailresponsible
data_type: 'integer'
default_value: 0
is_nullable: 0
=cut =cut
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
@ -68,6 +74,8 @@ __PACKAGE__->add_columns(
{ data_type => "text", is_nullable => 0 }, { data_type => "text", is_nullable => 0 },
"type", "type",
{ data_type => "text", is_nullable => 0 }, { data_type => "text", is_nullable => 0 },
"emailresponsible",
{ data_type => "integer", default_value => 0, is_nullable => 0 },
); );
=head1 PRIMARY KEY =head1 PRIMARY KEY
@ -142,7 +150,17 @@ __PACKAGE__->has_many(
); );
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-08 13:06:15
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:UXBzqO0vHPql4LYyXpgEQg # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:+mZZqLjQNwblb/EWW1alLQ
my %hint = (
relations => {
"jobsetinputalts" => "value"
}
);
sub json_hint {
return \%hint;
}
1; 1;

View file

@ -118,6 +118,17 @@ __PACKAGE__->table("Jobsets");
default_value: 300 default_value: 300
is_nullable: 0 is_nullable: 0
=head2 schedulingshares
data_type: 'integer'
default_value: 100
is_nullable: 0
=head2 fetcherrormsg
data_type: 'text'
is_nullable: 1
=cut =cut
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
@ -151,6 +162,10 @@ __PACKAGE__->add_columns(
{ data_type => "integer", default_value => 3, is_nullable => 0 }, { data_type => "integer", default_value => 3, is_nullable => 0 },
"checkinterval", "checkinterval",
{ data_type => "integer", default_value => 300, is_nullable => 0 }, { data_type => "integer", default_value => 300, is_nullable => 0 },
"schedulingshares",
{ data_type => "integer", default_value => 100, is_nullable => 0 },
"fetcherrormsg",
{ data_type => "text", is_nullable => 1 },
); );
=head1 PRIMARY KEY =head1 PRIMARY KEY
@ -271,8 +286,42 @@ __PACKAGE__->belongs_to(
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" }, { is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
); );
=head2 starredjobs
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 Type: has_many
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tsGR8MhZRIUeNwpcVczMUw
Related object: L<Hydra::Schema::StarredJobs>
=cut
__PACKAGE__->has_many(
"starredjobs",
"Hydra::Schema::StarredJobs",
{
"foreign.jobset" => "self.name",
"foreign.project" => "self.project",
},
undef,
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:DTAGxP5RFvcNxP/ciJGo4Q
my %hint = (
columns => [
"enabled",
"errormsg",
"fetcherrormsg",
"emailoverride"
],
eager_relations => {
jobsetinputs => "name"
}
);
sub json_hint {
return \%hint;
}
1; 1;

View file

@ -0,0 +1,75 @@
use utf8;
package Hydra::Schema::NrBuilds;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Hydra::Schema::NrBuilds
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<NrBuilds>
=cut
__PACKAGE__->table("NrBuilds");
=head1 ACCESSORS
=head2 what
data_type: 'text'
is_nullable: 0
=head2 count
data_type: 'integer'
is_nullable: 0
=cut
__PACKAGE__->add_columns(
"what",
{ data_type => "text", is_nullable => 0 },
"count",
{ data_type => "integer", is_nullable => 0 },
);
=head1 PRIMARY KEY
=over 4
=item * L</what>
=back
=cut
__PACKAGE__->set_primary_key("what");
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-08-12 17:59:18
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CK8eJGC803nGj0wnete9xg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -226,6 +226,21 @@ __PACKAGE__->has_many(
undef, undef,
); );
=head2 starredjobs
Type: has_many
Related object: L<Hydra::Schema::StarredJobs>
=cut
__PACKAGE__->has_many(
"starredjobs",
"Hydra::Schema::StarredJobs",
{ "foreign.project" => "self.name" },
undef,
);
=head2 viewjobs =head2 viewjobs
Type: has_many Type: has_many
@ -267,15 +282,26 @@ Composing rels: L</projectmembers> -> username
__PACKAGE__->many_to_many("usernames", "projectmembers", "username"); __PACKAGE__->many_to_many("usernames", "projectmembers", "username");
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:RffghAo9jAaqYk41y1Sdqw # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:PdNQ2mf5azBB6nI+iAm8fQ
# These lines were loaded from '/home/rbvermaa/src/hydra/src/lib/Hydra/Schema/Projects.pm' found in @INC.
# They are now part of the custom portion of this file my %hint = (
# for you to hand-edit. If you do not either delete columns => [
# this section or remove that file from @INC, this section "name",
# will be repeated redundantly when you re-create this "displayname",
# file again via Loader! See skip_load_external to disable "description",
# this feature. "enabled",
"hidden",
"owner"
],
relations => {
releases => "name",
jobsets => "name"
}
);
sub json_hint {
return \%hint;
}
# You can replace this text with custom content, and it will be preserved on regeneration
1; 1;

View file

@ -0,0 +1,161 @@
use utf8;
package Hydra::Schema::StarredJobs;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
=head1 NAME
Hydra::Schema::StarredJobs
=cut
use strict;
use warnings;
use base 'DBIx::Class::Core';
=head1 COMPONENTS LOADED
=over 4
=item * L<Hydra::Component::ToJSON>
=back
=cut
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
=head1 TABLE: C<StarredJobs>
=cut
__PACKAGE__->table("StarredJobs");
=head1 ACCESSORS
=head2 username
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=head2 project
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=head2 jobset
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=head2 job
data_type: 'text'
is_foreign_key: 1
is_nullable: 0
=cut
__PACKAGE__->add_columns(
"username",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
"project",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
"jobset",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
"job",
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
);
=head1 PRIMARY KEY
=over 4
=item * L</username>
=item * L</project>
=item * L</jobset>
=item * L</job>
=back
=cut
__PACKAGE__->set_primary_key("username", "project", "jobset", "job");
=head1 RELATIONS
=head2 job
Type: belongs_to
Related object: L<Hydra::Schema::Jobs>
=cut
__PACKAGE__->belongs_to(
"job",
"Hydra::Schema::Jobs",
{ jobset => "jobset", name => "job", project => "project" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
);
=head2 jobset
Type: belongs_to
Related object: L<Hydra::Schema::Jobsets>
=cut
__PACKAGE__->belongs_to(
"jobset",
"Hydra::Schema::Jobsets",
{ name => "jobset", project => "project" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
);
=head2 project
Type: belongs_to
Related object: L<Hydra::Schema::Projects>
=cut
__PACKAGE__->belongs_to(
"project",
"Hydra::Schema::Projects",
{ name => "project" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
);
=head2 username
Type: belongs_to
Related object: L<Hydra::Schema::Users>
=cut
__PACKAGE__->belongs_to(
"username",
"Hydra::Schema::Users",
{ username => "username" },
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "CASCADE" },
);
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:naj5aKWuw8hLE6klmvW9Eg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View file

@ -135,6 +135,21 @@ __PACKAGE__->has_many(
undef, undef,
); );
=head2 starredjobs
Type: has_many
Related object: L<Hydra::Schema::StarredJobs>
=cut
__PACKAGE__->has_many(
"starredjobs",
"Hydra::Schema::StarredJobs",
{ "foreign.username" => "self.username" },
undef,
);
=head2 userroles =head2 userroles
Type: has_many Type: has_many
@ -161,14 +176,22 @@ Composing rels: L</projectmembers> -> project
__PACKAGE__->many_to_many("projects", "projectmembers", "project"); __PACKAGE__->many_to_many("projects", "projectmembers", "project");
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50 # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hy3MKvFxfL+1bTc7Hcb1zA # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Hv9Ukqud0d3uIUot0ErKeg
# These lines were loaded from '/home/rbvermaa/src/hydra/src/lib/Hydra/Schema/Users.pm' found in @INC.
# They are now part of the custom portion of this file my %hint = (
# for you to hand-edit. If you do not either delete columns => [
# this section or remove that file from @INC, this section "fullname",
# will be repeated redundantly when you re-create this "emailaddress",
# file again via Loader! See skip_load_external to disable "username"
# this feature. ],
relations => {
userroles => "role"
}
);
sub json_hint {
return \%hint;
}
1; 1;

View file

@ -19,31 +19,83 @@ sub escape {
sub process { sub process {
my ($self, $c) = @_; my ($self, $c) = @_;
my $res = "[\n"; my %perSystem;
foreach my $pkg (@{$c->stash->{nixPkgs}}) { foreach my $pkg (@{$c->stash->{nixPkgs}}) {
my $build = $pkg->{build}; my $build = $pkg->{build};
$res .= " # $pkg->{name}\n"; $perSystem{$build->system}->{$build->get_column('job')} = $pkg;
$res .= " { type = \"derivation\";\n";
$res .= " name = " . escape ($build->get_column("releasename") or $build->nixname) . ";\n";
$res .= " system = " . (escape $build->system) . ";\n";
$res .= " outPath = " . (escape $pkg->{outPath}) . ";\n";
$res .= " meta = {\n";
$res .= " description = " . (escape $build->description) . ";\n"
if $build->description;
$res .= " longDescription = " . (escape $build->longdescription) . ";\n"
if $build->longdescription;
$res .= " license = " . (escape $build->license) . ";\n"
if $build->license;
$res .= " };\n";
$res .= " }\n";
} }
$res .= "]\n"; my $res = <<EOF;
{ system ? builtins.currentSystem }:
let
mkFakeDerivation = attrs: outputs:
let
outputNames = builtins.attrNames outputs;
common = attrs // outputsSet //
{ type = "derivation";
outputs = outputNames;
all = outputsList;
};
outputToAttrListElement = outputName:
{ name = outputName;
value = common // {
inherit outputName;
outPath = builtins.getAttr outputName outputs;
};
};
outputsList = map outputToAttrListElement outputNames;
outputsSet = builtins.listToAttrs outputsList;
in outputsSet;
in
EOF
my $first = 1;
foreach my $system (keys %perSystem) {
$res .= "else " if !$first;
$res .= "if system == ${\escape $system} then {\n\n";
foreach my $job (keys $perSystem{$system}) {
my $pkg = $perSystem{$system}->{$job};
my $build = $pkg->{build};
$res .= " # Hydra build ${\$build->id}\n";
my $attr = $build->get_column('job');
$attr =~ s/\./-/g;
$res .= " ${\escape $attr} = (mkFakeDerivation {\n";
$res .= " type = \"derivation\";\n";
$res .= " name = ${\escape ($build->get_column('releasename') or $build->nixname)};\n";
$res .= " system = ${\escape $build->system};\n";
$res .= " meta = {\n";
$res .= " description = ${\escape $build->description};\n"
if $build->description;
$res .= " longDescription = ${\escape $build->longdescription};\n"
if $build->longdescription;
$res .= " license = ${\escape $build->license};\n"
if $build->license;
$res .= " maintainers = ${\escape $build->maintainers};\n"
if $build->maintainers;
$res .= " };\n";
$res .= " } {\n";
my @outputNames = sort (keys $pkg->{outputs});
$res .= " ${\escape $_} = ${\escape $pkg->{outputs}->{$_}};\n" foreach @outputNames;
my $out = defined $pkg->{outputs}->{"out"} ? "out" : $outputNames[0];
$res .= " }).$out;\n\n";
}
$res .= "}\n\n";
$first = 0;
}
$res .= "else " if !$first;
$res .= "{}\n";
my $tar = Archive::Tar->new; my $tar = Archive::Tar->new;
$tar->add_data("channel/channel-name", ($c->stash->{channelName} or "unnamed-channel"), {mtime => 0}); $tar->add_data("channel/channel-name", ($c->stash->{channelName} or "unnamed-channel"), {mtime => 1});
$tar->add_data("channel/default.nix", $res, {mtime => 0}); $tar->add_data("channel/default.nix", $res, {mtime => 1});
my $tardata = $tar->write; my $tardata = $tar->write;
my $bzip2data; my $bzip2data;

View file

@ -8,7 +8,7 @@ sub process {
my ($self, $c) = @_; my ($self, $c) = @_;
$c->response->content_encoding("utf-8"); $c->response->content_encoding("utf-8");
$c->response->content_type('text/plain') unless $c->response->content_type() ne ""; $c->response->content_type('text/plain') unless $c->response->content_type() ne "";
$self->SUPER::process($c); $c->response->body($c->stash->{plain}->{data});
} }
1; 1;

View file

@ -8,17 +8,18 @@ __PACKAGE__->config(
TEMPLATE_EXTENSION => '.tt', TEMPLATE_EXTENSION => '.tt',
PRE_CHOMP => 1, PRE_CHOMP => 1,
POST_CHOMP => 1, POST_CHOMP => 1,
expose_methods => [qw/log_exists ellipsize/]); expose_methods => [qw/buildLogExists buildStepLogExists/]);
sub log_exists { sub buildLogExists {
my ($self, $c, $drvPath) = @_; my ($self, $c, $build) = @_;
my $x = getDrvLogPath($drvPath); my @outPaths = map { $_->path } $build->buildoutputs->all;
return defined $x; return defined findLog($c, $build->drvpath, @outPaths);
} }
sub ellipsize { sub buildStepLogExists {
my ($self, $c, $s, $n) = @_; my ($self, $c, $step) = @_;
return length $s <= $n ? $s : substr($s, 0, $n - 3) . "..."; my @outPaths = map { $_->path } $step->buildstepoutputs->all;
return defined findLog($c, $step->drvpath, @outPaths);
} }
1; 1;

View file

@ -7,6 +7,7 @@
[% project = build.project %] [% project = build.project %]
[% jobset = build.jobset %] [% jobset = build.jobset %]
[% job = build.job %] [% job = build.job %]
[% isAggregate = constituents.size > 0 %]
[% BLOCK renderOutputs %] [% BLOCK renderOutputs %]
[% start=1; FOREACH output IN outputs %] [% start=1; FOREACH output IN outputs %]
@ -22,7 +23,7 @@
<tbody> <tbody>
[% FOREACH step IN build.buildsteps %] [% FOREACH step IN build.buildsteps %]
[% IF ( type == "All" ) || ( type == "Failed" && step.status != 0 ) || ( type == "Running" && step.busy == 1 ) %] [% IF ( type == "All" ) || ( type == "Failed" && step.status != 0 ) || ( type == "Running" && step.busy == 1 ) %]
[% has_log = log_exists(step.drvpath); [% has_log = buildStepLogExists(step);
log = c.uri_for('/build' build.id 'nixlog' step.stepnr); %] log = c.uri_for('/build' build.id 'nixlog' step.stepnr); %]
<tr> <tr>
<td>[% step.stepnr %]</td> <td>[% step.stepnr %]</td>
@ -67,7 +68,40 @@
[% END %] [% END %]
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="dropdown">
<a class="dropdown-toggle actions" data-toggle="dropdown" href="#">
Actions
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
[% IF build.nixexprinput %]
<li><a href="#reproduce" data-toggle="modal">Reproduce locally</a></li>
[% END %]
[% IF c.user_exists %]
[% IF available %]
[% IF build.keep %]
<li><a href="[% c.uri_for('/build' build.id 'keep' 0) %]">Unkeep</a></li>
[% ELSE %]
<li><a href="[% c.uri_for('/build' build.id 'keep' 1) %]">Keep</a></li>
[% END %]
[% END %]
[% IF build.finished %]
<li><a href="[% c.uri_for('/build' build.id 'restart') %]">Restart</a></li>
[% ELSE %]
<li><a href="[% c.uri_for('/build' build.id 'cancel') %]">Cancel</a></li>
[% END %]
[% IF available && project.releases %]
[% INCLUDE menuItem
uri = "#add-to-release"
title = "Add to release"
modal = 1 %]
[% END %]
[% END %]
</ul>
</li>
<li class="active"><a href="#tabs-summary" data-toggle="tab">Summary</a></li> <li class="active"><a href="#tabs-summary" data-toggle="tab">Summary</a></li>
[% IF isAggregate %]<li><a href="#tabs-constituents" data-toggle="tab">Constituents</a></li>[% END %]
<li><a href="#tabs-details" data-toggle="tab">Details</a></li> <li><a href="#tabs-details" data-toggle="tab">Details</a></li>
<li><a href="#tabs-buildinputs" data-toggle="tab">Inputs</a></li> <li><a href="#tabs-buildinputs" data-toggle="tab">Inputs</a></li>
[% IF build.buildsteps %]<li><a href="#tabs-buildsteps" data-toggle="tab">Build steps</a></li>[% END %] [% IF build.buildsteps %]<li><a href="#tabs-buildsteps" data-toggle="tab">Build steps</a></li>[% END %]
@ -81,26 +115,6 @@
<div id="tabs-summary" class="tab-pane active"> <div id="tabs-summary" class="tab-pane active">
[% IF build.nixexprinput %]
[% WRAPPER makePopover title="Reproduce locally" classes="btn-info pull-right" placement="left" %]
[% url = c.uri_for('/build' build.id 'reproduce') %]
<p>You can reproduce this build on your own machine by
downloading <a [% HTML.attributes(href => url) %]>a script</a>
that checks out all inputs of the build and then invokes Nix
to perform the build. This script requires that you have Nix
on your system.</p>
<p>To download and execute the script from the command line,
run the following command:</p>
<pre>
<span class="shell-prompt">$ </span>bash <(curl <a [% HTML.attributes(href => url) %]>[% HTML.escape(url) %]</a>)
</pre>
[% END %]
[% END %]
<table> <table>
<tr> <tr>
<td> <td>
@ -114,7 +128,28 @@
</tr> </tr>
<tr> <tr>
<th>Status:</th> <th>Status:</th>
<td>[% INCLUDE renderStatus build=build icon=0 %]</td> <td>
[% INCLUDE renderStatus build=build icon=0 %]
[% IF isAggregate;
nrConstituents = 0;
nrFinished = 0;
nrFailedConstituents = 0;
FOREACH b IN constituents;
nrConstituents = nrConstituents + 1;
IF b.finished; nrFinished = nrFinished + 1; END;
IF b.finished && b.buildstatus != 0; nrFailedConstituents = nrFailedConstituents + 1; END;
END;
%];
[%+ IF nrFinished == nrMembers && nrFailedConstituents == 0 %]
all [% nrConstituents %] constituent builds succeeded
[% ELSE %]
[% nrFailedConstituents %] out of [% nrConstituents %] constituent builds failed
[% IF nrFinished < nrConstituents %]
([% nrConstituents - nrFinished %] still pending)
[% END %]
[% END %]
[% END %]
</td>
</tr> </tr>
<tr> <tr>
<th>System:</th> <th>System:</th>
@ -146,7 +181,7 @@
<td>[% IF cachedBuild; INCLUDE renderFullBuildLink build=cachedBuild; ELSE %]<em>unknown</em>[% END %]</td> <td>[% IF cachedBuild; INCLUDE renderFullBuildLink build=cachedBuild; ELSE %]<em>unknown</em>[% END %]</td>
</tr> </tr>
[% END %] [% END %]
[% IF build.finished %] [% IF !isAggregate && build.finished %]
<tr> <tr>
<th>Duration:</th> <th>Duration:</th>
<td>[% actualBuild = build.iscachedbuild ? cachedBuild : build; <td>[% actualBuild = build.iscachedbuild ? cachedBuild : build;
@ -154,7 +189,7 @@
finished at [% INCLUDE renderDateTime timestamp = actualBuild.stoptime %]</td> finished at [% INCLUDE renderDateTime timestamp = actualBuild.stoptime %]</td>
</tr> </tr>
[% END %] [% END %]
[% IF log_exists(build.drvpath) %] [% IF !isAggregate && buildLogExists(build) %]
<tr> <tr>
<th>Logfile:</th> <th>Logfile:</th>
<td> <td>
@ -169,20 +204,7 @@
</tr> </tr>
</table> </table>
[% IF c.user_exists && available %] [% IF build.buildproducts && !isAggregate %]
<br/>
<form class="form-horizontal" action="[% c.uri_for('/build' build.id 'add-to-release') %]" method="post">
<div class="control-group">
<label class="control-label">Add to release</label>
<div class="controls">
<input type="text" class="input" name="name"></input>
<button type="submit" class="btn btn-success">Apply</button>
</div>
</div>
</form>
[% END %]
[% IF build.buildproducts %]
<h3>Build products</h3> <h3>Build products</h3>
@ -251,6 +273,18 @@
</div> </div>
[% IF isAggregate %]
<div id="tabs-constituents" class="tab-pane">
<p>This build is an aggregate of the following builds:</p>
[% INCLUDE renderBuildList builds=constituents hideProjectName=1 hideJobsetName=1 %]
</div>
[% END %]
<div id="tabs-details" class="tab-pane"> <div id="tabs-details" class="tab-pane">
<table class="info-table"> <table class="info-table">
@ -380,8 +414,8 @@
<div id="placeholder" style="width:800px;height:400px;"></div> <div id="placeholder" style="width:800px;height:400px;"></div>
<div id="overview" style="margin-left:50px;margin-top:20px;width:600px;height:50px"></div> <div id="overview" style="margin-left:50px;margin-top:20px;width:600px;height:50px"></div>
<script src="/static/js/flot/jquery.flot.js" type="text/javascript"></script> <script src="[% c.uri_for("/static/js/flot/jquery.flot.js") %]" type="text/javascript"></script>
<script src="/static/js/flot/jquery.flot.selection.js" type="text/javascript"></script> <script src="[% c.uri_for("/static/js/flot/jquery.flot.selection.js") %]" type="text/javascript"></script>
<script type="text/javascript"> <script type="text/javascript">
$(function() { $(function() {
var d = []; var d = [];
@ -524,4 +558,58 @@
</div> </div>
[% IF c.user_exists && available && project.releases %]
<div id="add-to-release" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
<form class="form-horizontal" action="[% c.uri_for('/build' build.id 'add-to-release') %]" method="post">
<div class="modal-body">
<div class="control-group">
<label class="control-label">Add to release</label>
<div class="controls">
<select class="span2" name="name">
[% FOREACH r IN project.releases %]
<option>[% HTML.escape(r.name) %]</option>
[% END %]
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Add</button>
<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
</div>
</form>
</div>
[% END %]
<div id="reproduce" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
[% url = c.uri_for('/build' build.id 'reproduce') %]
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Reproduce this build</h3>
</div>
<div class="modal-body">
<p>You can reproduce this build on your own machine by downloading
<a [% HTML.attributes(href => url) %]>a script</a> that checks out
all inputs of the build and then invokes Nix to perform the build.
This script requires that you have Nix on your system.</p>
<p>To download and execute the script from the command line, run the
following command:</p>
<pre>
<span class="shell-prompt">$ </span>bash <(curl <a [% HTML.attributes(href => url) %]>[% HTML.escape(url) %]</a>)
</pre>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-primary" data-dismiss="modal">Close</a>
</div>
</div>
[% END %] [% END %]

View file

@ -60,7 +60,6 @@ install the package simply by clicking on the packages below.</p>
[% ELSE %] [% ELSE %]
[% HTML.escape(b.description) %] [% HTML.escape(b.description) %]
[% END %] [% END %]
[% IF pkg.outName != 'out' %] [[% pkg.outName %]][% END %]
</td> </td>
</tr> </tr>

View file

@ -1,51 +0,0 @@
[% WRAPPER layout.tt title="Clone build ${build.id}" %]
[% PROCESS common.tt %]
[% USE HTML %]
[% edit=1 %]
<p>Cloning allows you to perform a build with modified inputs.</p>
<form action="[% c.uri_for('/build' build.id 'clone' 'submit') %]" method="post">
<h2>Nix expression</h2>
<p>Evaluate job <tt><input type="text" class="string"
name="jobname" [% HTML.attributes(value => build.job.name) %]
/></tt> in Nix expression <tt><input type="text" class="string"
name="nixexprpath" [% HTML.attributes(value => build.nixexprpath) %]
/></tt> in input <tt><input type="text" class="string"
name="nixexprinput" [% HTML.attributes(value => build.nixexprinput)
%] /></tt>.</p>
<h2>Build inputs</h2>
<table class="table table-condensed table-striped">
<thead>
<tr><th>Name</th><th>Type</th><th>Value</th></tr>
</thead>
<tbody>
[% FOREACH input IN build.inputs %]
<tr>
<td><tt>[% input.name %]<input type="hidden" [% HTML.attributes(name => "input-$input.name-name" value => input.name) %] /></tt></td>
<td>
[% INCLUDE renderSelection curValue=input.type param="input-$input.name-type" options=inputTypes %]
</td>
<td>
<tt><input type="text" class="string" name="input-[% input.name %]-value"
[% IF input.type == "build" || input.type == "sysbuild" %]
[% build = input.dependency %]
[% HTML.attributes(value => build.project.name _ ':' _ build.jobset.name _ ':' _ build.job.name _ '[id="'_ build.id _ '"]' ) %]
[% ELSE %]
[% HTML.attributes(value => input.value || input.uri) %]
[% END %] /></tt>
</td>
</tr>
[% END %]
</tbody>
</table>
<p><button type="submit"><img alt="Add" src="/static/images/success.gif" />Add to queue</button></p>
</form>
[% END %]

View file

@ -1,24 +0,0 @@
[% WRAPPER layout.tt title="Clone jobset $jobset.project.name:$jobset.name" %]
[% PROCESS common.tt %]
[% USE HTML %]
[% edit=1 %]
<form class="form-horizontal" action="[% c.uri_for('/jobset' jobset.project.name jobset.name 'clone' 'submit') %]" method="post">
<fieldset>
<div class="control-group">
<label class="control-label">New name</label>
<div class="controls">
<input type="text" class="span3" name="newjobset" value=""></input>
</div>
</div>
<div class="form-actions">
<input type="submit" value="Submit" class="btn btn-primary" />
</div>
</fieldset>
</form>
[% END %]

View file

@ -20,7 +20,7 @@ BLOCK renderJobsetName %]
BLOCK renderJobName %] BLOCK renderJobName %]
<a [% IF inRow %]class="row-link"[% END %] href="[% c.uri_for('/job' project jobset job) %]"><tt>[% job %]</tt></a> <a [% IF inRow %]class="row-link"[% END %] href="[% c.uri_for('/job' project jobset job) %]">[% job %]</a>
[% END; [% END;
@ -40,9 +40,9 @@ END;
BLOCK renderDuration; BLOCK renderDuration;
IF duration >= 24 * 60 * 60; duration div (24 * 60 * 60) %]d [% END; IF duration >= 24 * 60 * 60; duration div (24 * 60 * 60) %]d&nbsp;[% END;
IF duration >= 60 * 60; duration div (60 * 60) % 24 %]h [% END; IF duration >= 60 * 60; duration div (60 * 60) % 24 %]h&nbsp;[% END;
IF duration >= 60; duration div 60 % 60 %]m [% END; IF duration >= 60; duration div 60 % 60 %]m&nbsp;[% END;
duration % 60 %]s[% duration % 60 %]s[%
END; END;
@ -64,12 +64,9 @@ BLOCK renderBuildListHeader %]
[% IF !hideJobName %] [% IF !hideJobName %]
<th>Job</th> <th>Job</th>
[% END %] [% END %]
<th>Release Name</th> <th>Release name</th>
<th>System</th> <th>System</th>
<th>[% IF showSchedulingInfo %]Queued at[% ELSE %]Finished at[% END %]</th> <th>[% IF showSchedulingInfo %]Queued at[% ELSE %]Finished at[% END %]</th>
[% IF showStatusChange %]
<th class="headerSortUp">Last status change</th>
[% END %]
[% IF showDescription %] [% IF showDescription %]
<th>Description</th> <th>Description</th>
[% END %] [% END %]
@ -99,25 +96,14 @@ BLOCK renderBuildListBody;
[% END %] [% END %]
<td>[% !showSchedulingInfo and build.get_column('releasename') ? build.get_column('releasename') : build.nixname %]</td> <td>[% !showSchedulingInfo and build.get_column('releasename') ? build.get_column('releasename') : build.nixname %]</td>
<td class="nowrap"><tt>[% build.system %]</tt></td> <td class="nowrap"><tt>[% build.system %]</tt></td>
<td class="nowrap">[% date.format(showSchedulingInfo ? build.timestamp : build.stoptime, '%Y-%m-%d %H:%M:%S') %]</td> <td class="nowrap">[% t = showSchedulingInfo ? build.timestamp : build.stoptime; IF t; date.format(showSchedulingInfo ? build.timestamp : build.stoptime, '%Y-%m-%d %H:%M:%S'); ELSE; "-"; END %]</td>
[% IF showStatusChange %]
<td>
[% IF build.get_column('statusChangeTime') %]
<a href="[% c.uri_for('/build' build.get_column('statusChangeId')) %]">
[% date.format(build.get_column('statusChangeTime'), '%Y-%m-%d %H:%M:%S') %]
</a>
[% ELSE %]
<em>never</em>
[% END %]
</td>
[% END %]
[% IF showDescription %] [% IF showDescription %]
<td>[% build.description %]</td> <td>[% build.description %]</td>
[% END %] [% END %]
</tr> </tr>
[% END; [% END;
IF linkToAll %] IF linkToAll %]
<td class="centered" colspan="5"><a href="[% linkToAll %]"><em>More...</em></a></td></tr> <tr><td class="centered" colspan="5"><a href="[% linkToAll %]"><em>More...</em></a></td></tr>
[% END; [% END;
END; END;
@ -144,7 +130,7 @@ END;
BLOCK maybeLink; BLOCK maybeLink;
IF uri %]<a [% HTML.attributes(href => uri, class => class) %][% IF confirmmsg %]onclick="javascript:return confirm('[% confirmmsg %]')"[% END %]>[% content %]</a>[% ELSE; content; END; IF uri %]<a [% HTML.attributes(href => uri, class => class); IF confirmmsg +%] onclick="javascript:return confirm('[% confirmmsg %]')"[% END %]>[% content %]</a>[% ELSE; content; END;
END; END;
@ -164,7 +150,7 @@ BLOCK renderSelection;
[% ELSE %] [% ELSE %]
<select [% HTML.attributes(id => param, name => param) %]> <select [% HTML.attributes(id => param, name => param) %]>
[% FOREACH name IN options.keys.sort %] [% FOREACH name IN options.keys.sort %]
<option [% HTML.attributes(value => name) %] [% IF name == curValue; "selected='selected'"; END %]>[% options.$name %]</option> <option [% IF name == curValue; "selected='selected'"; END; " "; HTML.attributes(value => name) %]>[% options.$name %]</option>
[% END %] [% END %]
</select> </select>
[% END; [% END;
@ -195,24 +181,24 @@ BLOCK renderBuildStatusIcon;
buildstatus = buildstatus != undef ? buildstatus : build.buildstatus; buildstatus = buildstatus != undef ? buildstatus : build.buildstatus;
IF finished; IF finished;
IF buildstatus == 0 %] IF buildstatus == 0 %]
<img src="/static/images/checkmark_[% size %].png" alt="Succeeded" /> <img src="[% c.uri_for("/static/images/checkmark_${size}.png") %]" alt="Succeeded" />
[% ELSIF buildstatus == 1 %] [% ELSIF buildstatus == 1 %]
<img src="/static/images/error_[% size %].png" alt="Failed" /> <img src="[% c.uri_for("/static/images/error_${size}.png") %]" alt="Failed" />
[% ELSIF buildstatus == 2 %] [% ELSIF buildstatus == 2 || buildstatus == 5 %]
<img src="/static/images/dependency_[% size %].png" alt="Dependency failed" /> <img src="[% c.uri_for("/static/images/dependency_${size}.png") %]" alt="Dependency failed" />
[% ELSIF buildstatus == 3 %]
<img src="[% c.uri_for("/static/images/warning_${size}.png") %]" alt="Aborted" />
[% ELSIF buildstatus == 4 %] [% ELSIF buildstatus == 4 %]
<img src="/static/images/cancelled_[% size %].png" alt="Cancelled" /> <img src="[% c.uri_for("/static/images/forbidden_${size}.png") %]" alt="Cancelled" />
[% ELSIF buildstatus == 5 %]
<img src="/static/images/error_[% size %].png" alt="Failed" />
[% ELSIF buildstatus == 6 %] [% ELSIF buildstatus == 6 %]
<img src="/static/images/error_[% size %].png" alt="Failed (with result)" /> <img src="[% c.uri_for("/static/images/error_${size}.png") %]" alt="Failed (with result)" />
[% ELSE %] [% ELSE %]
<img src="/static/images/error_[% size %].png" alt="Failed" /> <img src="[% c.uri_for("/static/images/error_${size}.png") %]" alt="Failed" />
[% END; [% END;
ELSIF busy %] ELSIF busy %]
<img src="/static/images/help_[% size %].png" alt="Busy" /> <img src="[% c.uri_for("/static/images/help_${size}.png") %]" alt="Busy" />
[% ELSE %] [% ELSE %]
<img src="/static/images/help_[% size %].png" alt="Scheduled" /> <img src="[% c.uri_for("/static/images/help_${size}.png") %]" alt="Scheduled" />
[% END; [% END;
END; END;
@ -225,17 +211,15 @@ BLOCK renderStatus;
<strong>Success</strong> <strong>Success</strong>
[% ELSIF buildstatus == 1 %] [% ELSIF buildstatus == 1 %]
<span class="error">Build returned a non-zero exit code</span> <span class="error">Build returned a non-zero exit code</span>
[% ELSIF buildstatus == 2 %] [% ELSIF buildstatus == 2 || buildstatus == 5 %]
<span class="error">A dependency of the build failed</span> <span class="error">A dependency of the build failed</span>
[% ELSIF buildstatus == 4 %] [% ELSIF buildstatus == 4 %]
<span class="error">Cancelled by user</span> <span class="error">Cancelled by user</span>
[% ELSIF buildstatus == 5 %]
<span class="error">Build inhibited because a dependency previously failed to build</span>
[% ELSIF buildstatus == 6 %] [% ELSIF buildstatus == 6 %]
<span class="error">Build failed (with result)</span> <span class="error">Build failed (with result)</span>
[% ELSE %] [% ELSE %]
<span class="error">Build failed</span> <span class="error">Aborted</span>
(see <a href="#nix-error">below</a>) (Hydra failure; see <a href="#nix-error">below</a>)
[% END; [% END;
ELSIF build.busy %] ELSIF build.busy %]
<strong>Build in progress</strong> <strong>Build in progress</strong>
@ -246,24 +230,15 @@ BLOCK renderStatus;
END; END;
BLOCK renderInputValue;
IF input.type == "build" || input.type == "sysbuild";
INCLUDE renderFullBuildLink build=input.dependency;
ELSIF input.type == "string" || input.type == "boolean" %]
<tt>"[% input.value %]"</tt>
[% ELSE %]
<tt>[% input.uri %][% IF input.revision %] (r[% input.revision %])[% END %]</tt>
[% END;
END;
BLOCK renderShortInputValue; BLOCK renderShortInputValue;
IF input.type == "build" || input.type == "sysbuild" %] IF input.type == "build" || input.type == "sysbuild" %]
<a href="[% c.uri_for('/build' input.dependency.id) %]">[% input.dependency.id %]</a> <a href="[% c.uri_for('/build' input.dependency.id) %]">[% input.dependency.id %]</a>
[% ELSIF input.type == "string" || input.type == "boolean" %] [% ELSIF input.type == "string" %]
<tt>"[% input.value %]"</tt> <tt>"[% HTML.escape(input.value) %]"</tt>
[% ELSIF input.type == "nix" || input.type == "boolean" %]
<tt>[% HTML.escape(input.value) %]</tt>
[% ELSE %] [% ELSE %]
<tt>[% input.uri %][% IF input.revision %] (r[% input.revision %])[% END %]</tt> <tt>[% HTML.escape(input.uri) %][% IF input.revision %] (r[% HTML.escape(input.revision) %])[% END %]</tt>
[% END %] [% END %]
[% END; [% END;
@ -275,7 +250,7 @@ BLOCK renderDiffUri;
url = bi1.uri; url = bi1.uri;
path = url.replace(base, ''); path = url.replace(base, '');
IF url.match(base) %] IF url.match(base) %]
<a target="_new" href="[% m.uri.replace('_path_', path).replace('_1_', bi1.revision).replace('_2_', bi2.revision) %]">[% contents %]</a> <a target="_blank" href="[% m.uri.replace('_path_', path).replace('_1_', bi1.revision).replace('_2_', bi2.revision) %]">[% contents %]</a>
[% nouri = 0; [% nouri = 0;
END; END;
END; END;
@ -284,7 +259,7 @@ BLOCK renderDiffUri;
url = res.0; url = res.0;
branch = res.1; branch = res.1;
IF bi1.type == "hg" || bi1.type == "git" %] IF bi1.type == "hg" || bi1.type == "git" %]
<a target="_new" href="[% HTML.escape("/api/scmdiff?uri=$url&rev1=$bi1.revision&rev2=$bi2.revision&type=$bi1.type&branch=$branch") %]">[% contents %]</a> <a target="_blank" href="[% HTML.escape("/api/scmdiff?uri=$url&rev1=$bi1.revision&rev2=$bi2.revision&type=$bi1.type&branch=$branch") %]">[% contents %]</a>
[% ELSE; [% ELSE;
contents; contents;
END; END;
@ -305,13 +280,15 @@ BLOCK renderInputs; %]
<td> <td>
[% IF input.type == "build" || input.type == "sysbuild" %] [% IF input.type == "build" || input.type == "sysbuild" %]
[% INCLUDE renderFullBuildLink build=input.dependency %] [% INCLUDE renderFullBuildLink build=input.dependency %]
[% ELSIF input.type == "string" || input.type == "boolean" %] [% ELSIF input.type == "string" %]
<tt>"[% input.value %]"</tt> <tt>"[% HTML.escape(input.value) %]"</tt>
[% ELSIF input.type == "nix" || input.type == "boolean" %]
<tt>[% HTML.escape(input.value) %]</tt>
[% ELSE %] [% ELSE %]
<tt>[% input.uri %]</tt> <tt>[% HTML.escape(input.uri) %]</tt>
[% END %] [% END %]
</td> </td>
<td>[% IF input.revision %][% input.revision %][% END %]</td> <td>[% IF input.revision %][% HTML.escape(input.revision) %][% END %]</td>
<td><tt>[% input.path %]</tt></td> <td><tt>[% input.path %]</tt></td>
</tr> </tr>
[% END %] [% END %]
@ -372,10 +349,10 @@ BLOCK renderInputDiff; %]
BLOCK renderPager %] BLOCK renderPager %]
<ul class="pager"> <ul class="pager">
<li [% IF page == 1 %]class="disabled"[% END %]><a href="[% "$baseUri?page=1" %]">&laquo; First</a></li> <li [% IF page == 1 %]class="disabled"[% END %]><a href="[% "$baseUri?page=1" %]">« First</a></li>
<li [% IF page == 1 %]class="disabled"[% END %]><a href="[% "$baseUri?page="; (page - 1) %]">&lsaquo; Previous</a></li> <li [% IF page == 1 %]class="disabled"[% END %]><a href="[% "$baseUri?page="; (page - 1) %]"> Previous</a></li>
<li [% IF page * resultsPerPage >= total %]class="disabled"[% END %]><a href="[% "$baseUri?page="; (page + 1) %]">Next &rsaquo;</a></li> <li [% IF page * resultsPerPage >= total %]class="disabled"[% END %]><a href="[% "$baseUri?page="; (page + 1) %]">Next </a></li>
<li [% IF page * resultsPerPage >= total %]class="disabled"[% END %]><a href="[% "$baseUri?page="; (total - 1) div resultsPerPage + 1 %]">Last &raquo;</a></li> <li [% IF page * resultsPerPage >= total %]class="disabled"[% END %]><a href="[% "$baseUri?page="; (total - 1) div resultsPerPage + 1 %]">Last »</a></li>
</ul> </ul>
[% END; [% END;
@ -458,22 +435,10 @@ BLOCK renderLogLinks %]
BLOCK makeLazyTab %] BLOCK makeLazyTab %]
<div id="[% tabName %]" class="tab-pane"> <div id="[% tabName %]" class="tab-pane">
<center><img src="/static/images/ajax-loader.gif" alt="Loading..." /></center> <center><img src="[% c.uri_for("/static/images/ajax-loader.gif") %]" alt="Loading..." /></center>
</div> </div>
<script> <script>
$(function() { $(function() { makeLazyTab("[% tabName %]", "[% uri %]"); });
$('.nav-tabs').bind('show', function(e) {
var pattern = /#.+/gi;
var id = e.target.toString().match(pattern)[0];
if (id == "#[% tabName %]") {
$('#[% tabName %]').load("[% uri %]", function(response, status, xhr) {
if (status == "error") {
$('#[% tabName %]').html("<div class='alert alert-error'>Error loading tab: " + xhr.status + " " + xhr.statusText + "</div>");
}
});
}
});
});
</script> </script>
[% END; [% END;
@ -485,4 +450,18 @@ BLOCK makePopover %]
[% END; [% END;
BLOCK menuItem %]
<li class="[% IF "${root}${curUri}" == uri %]active[% END %]" [% IF confirmmsg %]onclick="javascript:return confirm('[% confirmmsg %]')"[% END %]>
<a [% HTML.attributes(href => uri) %] [%+ IF modal %]data-toggle="modal"[% END %]>
[% IF icon %]<i class="[% icon %] icon-black"></i> [%+ END %]
[% title %]
</a>
</li>
[% END;
BLOCK makeStar %]
<span class="star" data-post="[% starUri %]">[% IF starred; "★"; ELSE; "☆"; END %]</span>
[% END;
%] %]

42
src/root/dashboard.tt Normal file
View file

@ -0,0 +1,42 @@
[% WRAPPER layout.tt title="Dashboard" %]
[% PROCESS common.tt %]
<ul class="nav nav-tabs">
<li class="active"><a href="#tabs-starred-jobs" data-toggle="tab">Starred jobs</a></li>
</ul>
<div id="generic-tabs" class="tab-content">
<div id="tabs-starred-jobs" class="tab-pane active">
[% IF starredJobs.size > 0 %]
<p>Below are the 20 most recent builds of your starred jobs.</p>
<table class="table table-striped table-condensed">
<thead>
<tr><th>Job</th></tr>
</thead>
<tdata>
[% FOREACH j IN starredJobs %]
<tr>
<td>[% INCLUDE renderFullJobName project=j.job.get_column('project') jobset=j.job.get_column('jobset') job=j.job.name %]</td>
[% FOREACH b IN j.builds %]
<td><a href="[% c.uri_for('/build' b.id) %]">[% INCLUDE renderBuildStatusIcon size=16 build=b %]</a></td>
[% END %]
</tr>
[% END %]
</tdata>
</table>
[% ELSE %]
<div class="alert alert-warning">You have no starred jobs. You can add them by visiting a job page and clicking on the ☆ icon.</div>
[% END %]
</div>
</div>
[% END %]

View file

@ -12,7 +12,7 @@
<span id="[% done.${node.path} %]"><span class="dep-tree-line"> <span id="[% done.${node.path} %]"><span class="dep-tree-line">
[% IF node.buildStep %] [% IF node.buildStep %]
<a href="[% c.uri_for('/build' node.buildStep.get_column('build')) %]"><tt>[% node.name %]</tt></a> [% <a href="[% c.uri_for('/build' node.buildStep.get_column('build')) %]"><tt>[% node.name %]</tt></a> [%
IF log_exists(node.buildStep.drvpath); IF buildStepLogExists(node.buildStep);
INCLUDE renderLogLinks url=c.uri_for('/build' node.buildStep.get_column('build') 'nixlog' node.buildStep.stepnr); INCLUDE renderLogLinks url=c.uri_for('/build' node.buildStep.get_column('build') 'nixlog' node.buildStep.stepnr);
END %] END %]
[% ELSE %] [% ELSE %]

View file

@ -1,9 +1,10 @@
[% WRAPPER layout.tt title=(create ? "Create jobset in project $project.name" : "Editing jobset $project.name:$jobset.name") %] [% WRAPPER layout.tt title=(create ? "Create jobset in project $project.name" : clone ? "Cloning jobset $project.name:$jobset.name" : "Editing jobset $project.name:$jobset.name") %]
[% PROCESS common.tt %] [% PROCESS common.tt %]
[% USE format %]
[% BLOCK renderJobsetInputAlt %] [% BLOCK renderJobsetInputAlt %]
<button type="button" class="btn btn-warning" onclick='$(this).parents(".inputalt").remove()'><i class="icon-trash icon-white"></i></button> <button type="button" class="btn btn-warning" onclick='$(this).parents(".inputalt").remove()'><i class="icon-trash icon-white"></i></button>
<input type="text" [% HTML.attributes(value => alt.value, name => name) %]></input> <input type="text" [% HTML.attributes(value => alt.value, name => name) %]/>
<br /> <br />
[% END %] [% END %]
@ -11,51 +12,61 @@
<tr class="input [% extraClass %]" [% IF id %]id="[% id %]"[% END %]> <tr class="input [% extraClass %]" [% IF id %]id="[% id %]"[% END %]>
<td> <td>
<button type="button" class="btn btn-warning" onclick='$(this).parents(".input").remove()'><i class="icon-trash icon-white"></i></button> <button type="button" class="btn btn-warning" onclick='$(this).parents(".input").remove()'><i class="icon-trash icon-white"></i></button>
<tt><input type="text" id="[% baseName %]-name" name="[% baseName %]-name" [% HTML.attributes(value => input.name) %]></input> <input type="text" id="[% baseName %]-name" name="[% baseName %]-name" [% HTML.attributes(value => input.name) %]/>
</td> </td>
<td> <td>
[% INCLUDE renderSelection curValue=input.type param="$baseName-type" options=inputTypes %] [% INCLUDE renderSelection curValue=input.type param="$baseName-type" options=inputTypes %]
</td> </td>
<td class="inputalts" id="[% baseName %]"> <td class="inputalts" id="[% baseName %]">
[% FOREACH alt IN input.jobsetinputalts %] [% FOREACH alt IN input.search_related('jobsetinputalts', {}, { order_by => 'altnr' }) %]
<span class="inputalt"> <span class="inputalt">
[% INCLUDE renderJobsetInputAlt alt=alt name="$baseName-values" %] [% INCLUDE renderJobsetInputAlt alt=alt name="$baseName-values" %]
</span> </span>
[% END %] [% END %]
[% IF edit %]<button type="button" class="add-inputalt btn btn-success" onclick='return false'><i class="icon-plus icon-white"></i></button>[% END %] [% IF edit %]<button type="button" class="add-inputalt btn btn-success" onclick='return false'><i class="icon-plus icon-white"></i></button>[% END %]
</td> </td>
<td>
<input type="checkbox" id="[% baseName %]-emailresponsible" name="[% baseName %]-emailresponsible" [% IF input.emailresponsible; 'checked="checked"'; END %]/>
</td>
</tr> </tr>
[% END %] [% END %]
[% BLOCK renderJobsetInputs %] [% BLOCK renderJobsetInputs %]
<table class="table table-striped table-condensed"> <table class="table table-striped table-condensed">
<thead> <thead>
<tr><th>Input name</th><th>Type</th><th>Values</th></tr> <tr><th>Input name</th><th>Type</th><th>Values</th><th>Notify committers</th></tr>
</thead> </thead>
<tbody class="inputs"> <tbody class="inputs">
[% FOREACH input IN jobset.jobsetinputs %] [% FOREACH input IN jobset.jobsetinputs %]
[% INCLUDE renderJobsetInput input=input baseName="input-$input.name" %] [% INCLUDE renderJobsetInput input=input baseName="input-$input.name" %]
[% END %] [% END %]
<tr> <tr>
<td colspan="3" style="text-align: center;"><button type="button" class="add-input btn btn-success"><i class="icon-plus icon-white"></i> Add a new input</button></td <td colspan="4" style="text-align: center;"><button type="button" class="add-input btn btn-success"><i class="icon-plus icon-white"></i> Add a new input</button></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
[% END %] [% END %]
<form class="form-horizontal" action="[% IF create %][% c.uri_for('/project' project.name 'create-jobset/submit') %][% ELSE %][% c.uri_for('/jobset' project.name jobset.name 'submit') %][% END %]" method="post"> <form class="form-horizontal">
<fieldset> <fieldset>
<div class="control-group"> <div class="control-group">
<label class="control-label">State</label>
<div class="controls"> <div class="controls">
<label class="checkbox"> <div class="btn-group" data-toggle="buttons-radio">
<input type="checkbox" name="enabled" [% IF jobset.enabled; 'checked="checked"'; END %]></input>Enabled <input type="hidden" name="enabled" value="[% jobset.enabled %]" />
</label> <button type="button" class="btn" value="1">Enabled</button>
<button type="button" class="btn" value="2">One-shot</button>
<button type="button" class="btn" value="0">Disabled</button>
</div> </div>
</div>
</div>
<div class="control-group">
<div class="controls"> <div class="controls">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="visible" [% IF !jobset.hidden; 'checked="checked"'; END %]></input>Visible <input type="checkbox" name="visible" [% IF !jobset.hidden; 'checked="checked"'; END %]/>Visible
</label> </label>
</div> </div>
</div> </div>
@ -63,23 +74,23 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">Identifier</label> <label class="control-label">Identifier</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="name" [% HTML.attributes(value => jobset.name) %]></input> <input type="text" class="span3" name="name" [% HTML.attributes(value => clone ? "" : jobset.name) %]/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Description</label> <label class="control-label">Description</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="description" [% HTML.attributes(value => jobset.description) %]></input> <input type="text" class="span3" name="description" [% HTML.attributes(value => jobset.description) %]/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Nix expression</label> <label class="control-label">Nix expression</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="nixexprpath" [% HTML.attributes(value => jobset.nixexprpath) %]></input> <input type="text" class="span3" name="nixexprpath" [% HTML.attributes(value => jobset.nixexprpath) %]/>
in in
<input type="text" class="span3" name="nixexprinput" [% HTML.attributes(value => jobset.nixexprinput) %]></input> <input type="text" class="span3" name="nixexprinput" [% HTML.attributes(value => jobset.nixexprinput) %]/>
</div> </div>
</div> </div>
@ -87,17 +98,29 @@
<label class="control-label">Check interval</label> <label class="control-label">Check interval</label>
<div class="controls"> <div class="controls">
<div class="input-append"> <div class="input-append">
<input type="number" class="span3" name="checkinterval" [% HTML.attributes(value => jobset.checkinterval) %]></input> <input type="number" class="span3" name="checkinterval" [% HTML.attributes(value => jobset.checkinterval) %]/>
<span class="add-on">sec</span> <span class="add-on">sec</span>
</div> </div>
<span class="help-inline">(0 to disable polling)</span> <span class="help-inline">(0 to disable polling)</span>
</div> </div>
</div> </div>
<div class="control-group">
<label class="control-label">Scheduling shares</label>
<div class="controls">
<div class="input-append">
<input type="number" class="span3" name="schedulingshares" [% HTML.attributes(value => jobset.schedulingshares) %]/>
</div>
[% IF totalShares %]
<span class="help-inline">([% f = format("%.2f"); f(jobset.schedulingshares / totalShares * 100) %]% out of [% totalShares %] shares)</span>
[% END %]
</div>
</div>
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="enableemail" [% IF jobset.enableemail; 'checked="checked"'; END %]></input>Email notification <input type="checkbox" name="enableemail" [% IF jobset.enableemail; 'checked="checked"'; END %]/>Email notification
</label> </label>
</div> </div>
</div> </div>
@ -105,33 +128,21 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">Email override</label> <label class="control-label">Email override</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="emailoverride" [% HTML.attributes(value => jobset.emailoverride) %]></input> <input type="text" class="span3" name="emailoverride" [% HTML.attributes(value => jobset.emailoverride) %]/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Number of builds to keep</label> <label class="control-label">Number of evaluations to keep</label>
<div class="controls"> <div class="controls">
<input type="number" class="span3" name="keepnr" [% HTML.attributes(value => jobset.keepnr) %]></input> <input type="number" class="span3" name="keepnr" [% HTML.attributes(value => jobset.keepnr) %]/>
</div> </div>
</div> </div>
[% INCLUDE renderJobsetInputs %] [% INCLUDE renderJobsetInputs %]
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-primary"><i class="icon-ok icon-white"></i> [%IF create %]Create[% ELSE %]Apply changes[% END %]</button> <button id="submit-jobset" type="submit" class="btn btn-primary"><i class="icon-ok icon-white"></i> [%IF create || clone %]Create jobset[% ELSE %]Apply changes[% END %]</button>
[% IF !create %]
<button id="delete-jobset" type="submit" class="btn btn-danger" name="submit" value="delete">
<i class="icon-trash icon-white"></i>
Delete this jobset
</button>
<script type="text/javascript">
$("#delete-jobset").click(function() {
return confirm("Are you sure you want to delete this jobset?");
});
</script>
[% END %]
</div> </div>
</fieldset> </fieldset>
@ -144,7 +155,9 @@
[% INCLUDE renderJobsetInputAlt alt=alt %] [% INCLUDE renderJobsetInputAlt alt=alt %]
</tt> </tt>
<script type="text/javascript"> </form>
<script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
var id = 0; var id = 0;
@ -153,6 +166,7 @@
var x = $("#input-template").clone(true).attr("id", "").insertBefore($(this).parents("tr")).show(); var x = $("#input-template").clone(true).attr("id", "").insertBefore($(this).parents("tr")).show();
$("#input-template-name", x).attr("name", newid + "-name"); $("#input-template-name", x).attr("name", newid + "-name");
$("#input-template-type", x).attr("name", newid + "-type"); $("#input-template-type", x).attr("name", newid + "-type");
$("#input-template-emailresponsible", x).attr("name", newid + "-emailresponsible");
$("#input-template", x).attr("id", newid); $("#input-template", x).attr("id", newid);
return false; return false;
}); });
@ -162,8 +176,47 @@
$("input", x).attr("name", x.parents(".inputalts").attr("id") + "-values"); $("input", x).attr("name", x.parents(".inputalts").attr("id") + "-values");
}); });
}); });
</script>
</form> $("#submit-jobset").click(function() {
var formElements = $(this).parents("form").serializeArray();
var data = { 'inputs': {} };
var inputs = {};
for (var i = 0; i < formElements.length; i++) {
var elem = formElements[i];
var match = elem.name.match(/^input-(\w+)-(\w+)$/);
if (match === null) {
data[elem.name] = elem.value;
} else {
var baseName = match[1];
var param = match[2];
if (baseName === "template") {
continue;
}
if (!(baseName in inputs)) {
inputs[baseName] = {};
}
if (param === "name") {
data.inputs[elem.value] = inputs[baseName];
} else {
inputs[baseName][param] = elem.value;
}
}
}
redirectJSON({
[% IF create || clone %]
url: "[% c.uri_for('/jobset' project.name '.new') %]",
[% ELSE %]
url: "[% c.uri_for('/jobset' project.name jobset.name) %]",
[% END %]
data: JSON.stringify(data),
contentType: 'application/json',
type: 'PUT'
});
return false;
});
</script>
[% END %] [% END %]

View file

@ -1,19 +1,19 @@
[% WRAPPER layout.tt title=(create ? "New project" : "Editing project $project.name") %] [% WRAPPER layout.tt title=(create ? "New project" : "Editing project $project.name") %]
[% PROCESS common.tt %] [% PROCESS common.tt %]
<form class="form-horizontal" action="[% IF create %][% c.uri_for('/create-project/submit') %][% ELSE %][% c.uri_for('/project' project.name 'submit') %][% END %]" method="post"> <form class="form-horizontal">
<fieldset> <fieldset>
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="enabled" [% IF project.enabled; 'checked="checked"'; END %]></input>Enabled <input type="checkbox" name="enabled" [% IF project.enabled; 'checked="checked"'; END %]/>Enabled
</label> </label>
</div> </div>
<div class="controls"> <div class="controls">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="visible" [% IF !project.hidden; 'checked="checked"'; END %]></input>Visible in the list of projects <input type="checkbox" name="visible" [% IF !project.hidden; 'checked="checked"'; END %]/>Visible in the list of projects
</label> </label>
</div> </div>
</div> </div>
@ -21,58 +21,63 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">Identifier</label> <label class="control-label">Identifier</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="name" [% HTML.attributes(value => project.name) %]></input> <input type="text" class="span3" name="name" [% HTML.attributes(value => project.name) %]/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Display name</label> <label class="control-label">Display name</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="displayname" [% HTML.attributes(value => project.displayname) %]></input> <input type="text" class="span3" name="displayname" [% HTML.attributes(value => project.displayname) %]/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Description</label> <label class="control-label">Description</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="description" [% HTML.attributes(value => project.description) %]></input> <input type="text" class="span3" name="description" [% HTML.attributes(value => project.description) %]/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Homepage</label> <label class="control-label">Homepage</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="homepage" [% HTML.attributes(value => project.homepage) %]></input> <input type="text" class="span3" name="homepage" [% HTML.attributes(value => project.homepage) %]/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Owner</label> <label class="control-label">Owner</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="owner" [% HTML.attributes(value => project.owner.username || c.user.username) %]></input> <input type="text" class="span3" name="owner" [% HTML.attributes(value => project.owner.username || c.user.username) %]/>
</div> </div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-primary"> <button id="submit-project" type="submit" class="btn btn-primary">
<i class="icon-ok icon-white"></i> <i class="icon-ok icon-white"></i>
[%IF create %]Create[% ELSE %]Apply changes[% END %] [%IF create %]Create project[% ELSE %]Apply changes[% END %]
</button> </button>
[% IF !create %]
<button id="delete-project" type="submit" class="btn btn-danger" name="submit" value="delete">
<i class="icon-trash icon-white"></i>
Delete this project
</button>
<script type="text/javascript">
$("#delete-project").click(function() {
return confirm("Are you sure you want to delete this project?");
});
</script>
[% END %]
</div> </div>
</fieldset> </fieldset>
</form> </form>
<script type="text/javascript">
$("#submit-project").click(function() {
redirectJSON({
[% IF create %]
url: "[% c.uri_for('/project' '.new') %]",
[% ELSE %]
url: "[% c.uri_for('/project' project.name) %]",
[% END %]
data: $(this).parents("form").serialize(),
type: 'PUT'
});
return false;
});
</script>
[% END %] [% END %]

View file

@ -9,14 +9,14 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">Identifier</label> <label class="control-label">Identifier</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="name" [% HTML.attributes(value => release.name) %]></input> <input type="text" class="span3" name="name" [% HTML.attributes(value => release.name) %]/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Description</label> <label class="control-label">Description</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="description" [% HTML.attributes(value => release.description) %]></input> <input type="text" class="span3" name="description" [% HTML.attributes(value => release.description) %]/>
</div> </div>
</div> </div>
@ -30,7 +30,7 @@
<div class="releaseMember control-group"> <div class="releaseMember control-group">
<label class="control-label">Build [% m.build.id %] Label</label> <label class="control-label">Build [% m.build.id %] Label</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="member-[% m.build.id %]-description" [% HTML.attributes(value => m.description) %]></input> <input type="text" class="span3" name="member-[% m.build.id %]-description" [% HTML.attributes(value => m.description) %]/>
<button class="btn btn-warning" type="button" onclick='$(this).parents(".releaseMember").remove()'><i class="icon-trash icon-white"></i></button> <button class="btn btn-warning" type="button" onclick='$(this).parents(".releaseMember").remove()'><i class="icon-trash icon-white"></i></button>
</div> </div>
</div> </div>

View file

@ -26,14 +26,14 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">Identifier</label> <label class="control-label">Identifier</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="name" [% HTML.attributes(value => view.name) %]></input> <input type="text" class="span3" name="name" [% HTML.attributes(value => view.name) %]/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Description</label> <label class="control-label">Description</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="description" [% HTML.attributes(value => view.description) %]></input> <input type="text" class="span3" name="description" [% HTML.attributes(value => view.description) %]/>
</div> </div>
</div> </div>

View file

@ -2,7 +2,7 @@
[% USE HTML %] [% USE HTML %]
[% FOREACH error IN errors %] [% FOREACH error IN errors %]
<div class="alert alert-error">[% HTML.escape(error) %]</div> <div class="alert alert-error">[% HTML.escape(error).replace('\n', '<br/>') %]</div>
[% END %] [% END %]
[% END %] [% END %]

View file

@ -1,80 +0,0 @@
[% WRAPPER layout.tt title="Errors" %]
[% PROCESS common.tt %]
<p>This page provides a quick way to see how FUBARed your packages
are. It shows job expressions that dont evaluate properly and jobs
that dont build.</p>
[% haveErrors = 0 %]
[% IF brokenJobsets && brokenJobsets.size > 0; haveErrors = 1 %]
<h2>Evaluation errors in jobsets</h2>
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Name</th>
<th>Error</th>
</tr>
</thead>
<tdata>
[% FOREACH j IN brokenJobsets %]
<tr>
<td>[% INCLUDE renderFullJobsetName project=j.project.name jobset=j.name %]</td>
<td>
<pre class="error">[% HTML.escape(j.errormsg) %]</pre>
</td>
</tr>
[% END %]
</tdata>
</table>
[% END %]
[% IF brokenJobs && brokenJobs.size > 0; haveErrors = 1 %]
<h2>Evaluation errors in jobs</h2>
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Name</th>
<th>Error</th>
</tr>
</thead>
<tdata>
[% FOREACH j IN brokenJobs %]
<tr>
<td>[% INCLUDE renderFullJobName project=j.project.name jobset=j.jobset.name job=j.name %]</td>
<td>
<pre class="error">[% HTML.escape(j.errormsg) %]</pre>
</td>
</tr>
[% END %]
</tdata>
</table>
[% END %]
[% IF brokenBuilds && brokenBuilds.size > 0; haveErrors = 1 %]
<h2>Broken builds</h2>
[% INCLUDE renderBuildList builds=brokenBuilds showStatusChange=1 hideProjectName=project hideJobsetName=jobset hideJobName=job %]
[% END %]
[% IF !haveErrors %]
<p><strong>There are currently no problems.</strong></p>
[% END %]
[% END %]

View file

@ -1,9 +1,15 @@
[% WRAPPER layout.tt title="Job $project.name:$jobset.name:$job.name" %] [% WRAPPER layout.tt
title="Job $project.name:$jobset.name:$job.name"
starUri=c.uri_for(c.controller('Job').action_for('star'), c.req.captures)
%]
[% PROCESS common.tt %] [% PROCESS common.tt %]
[% hideProjectName=1 hideJobsetName=1 hideJobName=1 %] [% hideProjectName=1 hideJobsetName=1 hideJobName=1 %]
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="active"><a href="#tabs-status" data-toggle="tab">Status</a></li> <li class="active"><a href="#tabs-status" data-toggle="tab">Status</a></li>
[% IF constituentJobs.size > 0 %]
<li><a href="#tabs-constituents" data-toggle="tab">Constituents</a></li>
[% END %]
<li><a href="#tabs-links" data-toggle="tab">Links</a></li> <li><a href="#tabs-links" data-toggle="tab">Links</a></li>
</ul> </ul>
@ -12,7 +18,7 @@
<div id="tabs-status" class="tab-pane active"> <div id="tabs-status" class="tab-pane active">
[% IF lastBuilds.size != 0 %] [% IF lastBuilds.size != 0 %]
<h3>Lastest builds</h3> <h3>Lastest builds</h3>
[% INCLUDE renderBuildList builds=lastBuilds showStatusChange=0 [% INCLUDE renderBuildList builds=lastBuilds
linkToAll=c.uri_for('/job' project.name jobset.name job.name 'all') %] linkToAll=c.uri_for('/job' project.name jobset.name job.name 'all') %]
[% END %] [% END %]
[% IF queuedBuilds.size != 0 %] [% IF queuedBuilds.size != 0 %]
@ -21,12 +27,57 @@
[% END %] [% END %]
</div> </div>
[% IF constituentJobs.size > 0 %]
<div id="tabs-constituents" class="tab-pane">
<div class="well well-small">This is an <em>aggregate job</em>:
its success or failure is determined entirely by the result of
building its <em>constituent jobs</em>. The table below shows
the status of each constituent job for the [%
aggregates.keys.size %] most recent builds of the
aggregate.</div>
[% aggs = aggregates.keys.nsort.reverse %]
<table class="table table-striped table-condensed table-header-rotated">
<thead>
<tr>
<th>Job</th>
[% FOREACH agg IN aggs %]
<th class="rotate-45">
[% agg_ = aggregates.$agg %]
<div><span class="[% agg_.build.finished == 0 ? "text-info" : (agg_.build.buildstatus == 0 ? "text-success" : "text-warning") %] override-link">
<a href="[% c.uri_for('/build' agg) %]">[% agg %]</a>
</span></div></th>
[% END %]
</tr>
</thead>
<tbody>
[% FOREACH j IN constituentJobs %]
<tr>
<th style="width: 1em;">[% INCLUDE renderJobName project=project.name jobset=jobset.name job=j %]</th>
[% FOREACH agg IN aggs %]
<td>
[% r = aggregates.$agg.constituents.$j; IF r.id %]
<a href="[% c.uri_for('/build' r.id) %]">
[% INCLUDE renderBuildStatusIcon size=16 build=r %]
</a>
[% END %]
</td>
[% END %]
</tr>
[% END %]
</tbody>
</table>
</div>
[% END %]
<div id="tabs-links" class="tab-pane"> <div id="tabs-links" class="tab-pane">
<ul> <ul>
<li><a href="[% c.uri_for('/job' project.name jobset.name job.name 'latest') %]">Latest successful build</a></li> <li><a href="[% c.uri_for('/job' project.name jobset.name job.name 'latest') %]">Latest successful build</a></li>
[% FOREACH system IN systems %] <li><a href="[% c.uri_for('/job' project.name jobset.name job.name 'latest-finished') %]">Latest successful build from a finished evaluation</a></li>
<li><a href="[% c.uri_for('/job' project.name jobset.name job.name 'latest-for' system.system) %]">Latest successful build for <tt>[% system.system %]</tt></a></li>
[% END %]
</ul> </ul>
</div> </div>

View file

@ -4,10 +4,14 @@
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<a class="btn btn-primary dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-white icon-eye-open"></i> Compare to...</a> <a class="btn btn-primary dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-white icon-eye-open"></i> Compare to...</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="?">Preceding evaluation in the same jobset</tt></a></li> <li><a href="?">Preceding evaluation in this jobset</a></li>
<li class="divider"></li>
<li><a href="?compare=-[% 24 * 60 * 60 %]">This jobset <strong>one day</strong> earlier</a></li>
<li><a href="?compare=-[% 7 * 24 * 60 * 60 %]">This jobset <strong>one week</strong> earlier</a></li>
<li><a href="?compare=-[% 31 * 24 * 60 * 60 %]">This jobset <strong>one month</strong> earlier</a></li>
[% IF project.jobsets_rs.count > 1 %] [% IF project.jobsets_rs.count > 1 %]
<li class="divider"></li> <li class="divider"></li>
[% FOREACH j IN project.jobsets; IF j.name != jobset.name %] [% FOREACH j IN project.jobsets.sort('name'); IF j.name != jobset.name %]
<li><a href="?compare=[% j.name %]">Jobset <tt>[% project.name %]:[% j.name %]</tt></a></li> <li><a href="?compare=[% j.name %]">Jobset <tt>[% project.name %]:[% j.name %]</tt></a></li>
[% END; END %] [% END; END %]
[% END %] [% END %]
@ -19,81 +23,117 @@
project=otherEval.jobset.project.name jobset=otherEval.jobset.name %] evaluation <a href="[% project=otherEval.jobset.project.name jobset=otherEval.jobset.name %] evaluation <a href="[%
c.uri_for(c.controller('JobsetEval').action_for('view'), c.uri_for(c.controller('JobsetEval').action_for('view'),
[otherEval.id]) %]">[% otherEval.id %]</a>.</p> [otherEval.id]) %]">[% otherEval.id %]</a>.</p>
[% ELSE %]
<div class="alert">Couldn't find an evaluation to compare to.</div>
[% END %] [% END %]
<form class="form-search">
<input name="filter" type="text" class="input-large search-query" placeholder="Search jobs by name..." [% HTML.attributes(value => filter) %]/>
<input name="compare" type="hidden" [% HTML.attributes(value => otherEval.id) %]/>
<input name="full" type="hidden" [% HTML.attributes(value => full) %]/>
</form>
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="active"><a href="#tabs-status" data-toggle="tab">Job status</a></li> [% IF c.user_exists %]
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
Actions
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="[% c.uri_for(c.controller('JobsetEval').action_for('release'), [eval.id]) %]">Create a release from this evaluation</a></li>
<li><a href="[% c.uri_for(c.controller('JobsetEval').action_for('cancel'), [eval.id]) %]">Cancel all scheduled builds</a></li>
<li><a href="[% c.uri_for(c.controller('JobsetEval').action_for('restart_aborted'), [eval.id]) %]">Restart all aborted builds</a></li>
</ul>
</li>
[% END %]
[% IF aborted.size > 0 %]
<li><a href="#tabs-aborted" data-toggle="tab"><span class="text-warning">Aborted jobs ([% aborted.size %])</span></a></li>
[% END %]
[% IF nowFail.size > 0 %]
<li><a href="#tabs-now-fail" data-toggle="tab"><span class="text-warning">Newly-failing jobs ([% nowFail.size %])</span></a></li>
[% END %]
[% IF nowSucceed.size > 0 %]
<li><a href="#tabs-now-succeed" data-toggle="tab"><span class="text-success">Newly-succeeding jobs ([% nowSucceed.size %])</span></a></li>
[% END %]
[% IF new.size > 0 %]
<li><a href="#tabs-new" data-toggle="tab">New jobs ([% new.size %])</a></li>
[% END %]
[% IF removed.size > 0 %]
<li><a href="#tabs-removed" data-toggle="tab">Removed jobs ([% removed.size %])</a></li>
[% END %]
[% IF stillFail.size > 0 %]
<li><a href="#tabs-still-fail" data-toggle="tab">Still-failing jobs ([% stillFail.size %])</a></li>
[% END %]
[% IF stillSucceed.size > 0 %]
<li><a href="#tabs-still-succeed" data-toggle="tab">Still-succeeding jobs ([% stillSucceed.size %])</a></li>
[% END %]
[% IF unfinished.size > 0 %]
<li><a href="#tabs-unfinished" data-toggle="tab">Queued jobs ([% unfinished.size %])</a></li>
[% END %]
<li><a href="#tabs-inputs" data-toggle="tab">Inputs</a></li> <li><a href="#tabs-inputs" data-toggle="tab">Inputs</a></li>
</ul> </ul>
<div class="tab-content"> [% BLOCK renderSome %]
[% INCLUDE renderBuildListHeader unsortable=1 %]
<div id="tabs-status" class="tab-pane active"> [% size = builds.size; max = full ? size : 250; %]
[% BLOCK renderSome %]
[% size = builds.size; max = full ? size : 30; %]
[% INCLUDE renderBuildListBody builds=builds.slice(0, (size > max ? max : size) - 1) [% INCLUDE renderBuildListBody builds=builds.slice(0, (size > max ? max : size) - 1)
hideProjectName=1 hideJobsetName=1 %] hideProjectName=1 hideJobsetName=1 %]
[% IF size > max; params = c.req.params; params.full = 1 %] [% IF size > max; params = c.req.params; params.full = 1 %]
<tr><td class="centered" colspan="6"><a href="[% c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id], params) %]"><em>([% size - max %] more builds omitted)</em></a></td></tr> <tr><td class="centered" colspan="6"><a href="[% c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id], params) %]"><em>([% size - max %] more builds omitted)</em></a></td></tr>
[% END %] [% END %]
[% END %] [% INCLUDE renderBuildListFooter %]
[% END %]
[% INCLUDE renderBuildListHeader unsortable=1 %] <div class="tab-content">
[% IF unfinished.size > 0 %] <div id="tabs-aborted" class="tab-pane">
<tr><th class="subheader" colspan="6"><strong>Queued</strong> jobs</th></tr> [% INCLUDE renderSome builds=aborted %]
[% INCLUDE renderSome builds=unfinished %] </div>
[% END %]
[% IF new.size > 0 %] <div id="tabs-now-fail" class="tab-pane">
<tr><th class="subheader" colspan="6"><strong>New</strong> jobs</th></tr> [% INCLUDE renderSome builds=nowFail %]
</div>
<div id="tabs-now-succeed" class="tab-pane">
[% INCLUDE renderSome builds=nowSucceed %]
</div>
<div id="tabs-new" class="tab-pane">
[% INCLUDE renderSome builds=new %] [% INCLUDE renderSome builds=new %]
[% END %] </div>
[% IF removed.size > 0 %] <div id="tabs-removed" class="tab-pane">
<tr><th class="subheader" colspan="6"><strong>Removed</strong> jobs</th></tr> <table class="table table-striped table-condensed clickable-rows">
[% size = removed.size; max = full ? size : 30; %] <thead>
<tr><th>Job</th><th>System</th></tr>
</thead>
<tbody>
[% size = removed.size; max = full ? size : 250; %]
[% FOREACH j IN removed.slice(0,(size > max ? max : size) - 1) %] [% FOREACH j IN removed.slice(0,(size > max ? max : size) - 1) %]
<tr> <tr>
<td colspan="2"></td> <td>[% INCLUDE renderJobName project=project.name jobset=jobset.name job=j.job %]</td>
<td colspan="2">[% INCLUDE renderJobName project=project.name jobset=jobset.name job=j.job %]</td> <td><tt>[% j.system %]</tt></td>
<td colspan="2"><tt>[% j.system %]</tt></td>
</tr> </tr>
[% END %] [% END %]
[% IF size > max; params = c.req.params; params.full = 1 %] [% IF size > max; params = c.req.params; params.full = 1 %]
<tr><td class="centered" colspan="6"><a href="[% c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id], params) %]"><em>([% size - max %] more jobs omitted)</em></a></td></tr> <tr><td class="centered" colspan="2"><a href="[% c.uri_for(c.controller('JobsetEval').action_for('view'), [eval.id], params) %]"><em>([% size - max %] more jobs omitted)</em></a></td></tr>
[% END %]
[% END %] [% END %]
</tbody>
</table>
</div>
[% IF nowFail.size > 0 %] <div id="tabs-still-fail" class="tab-pane">
<tr><th class="subheader" colspan="6">Jobs that now <strong>fail</strong></th></tr>
[% INCLUDE renderSome builds=nowFail %]
[% END %]
[% IF nowSucceed.size > 0 %]
<tr><th class="subheader" colspan="6">Jobs that now <strong>succeed</strong></th></tr>
[% INCLUDE renderSome builds=nowSucceed %]
[% END %]
[% IF stillFail.size > 0 %]
<tr><th class="subheader" colspan="6">Jobs that still <strong>fail</strong></th></tr>
[% INCLUDE renderSome builds=stillFail %] [% INCLUDE renderSome builds=stillFail %]
[% END %] </div>
[% IF stillSucceed.size > 0 %] <div id="tabs-still-succeed" class="tab-pane">
<tr><th class="subheader" colspan="6">Jobs that still <strong>succeed</strong></th></tr>
[% INCLUDE renderSome builds=stillSucceed %] [% INCLUDE renderSome builds=stillSucceed %]
[% END %] </div>
[% INCLUDE renderBuildListFooter %]
[% IF c.user_exists %]
<p>
<a class="btn" href="[% c.uri_for(c.controller('JobsetEval').action_for('release'), [eval.id]) %]">Release</a>
</p>
[% END %]
<div id="tabs-unfinished" class="tab-pane">
[% INCLUDE renderSome builds=unfinished %]
</div> </div>
<div id="tabs-inputs" class="tab-pane"> <div id="tabs-inputs" class="tab-pane">

View file

@ -1,15 +1,69 @@
[% PROCESS common.tt %] [% PROCESS common.tt; USE Math %]
<p>This jobset currently contains the following [% activeJobs.size %] jobs: <form class="form-search" id="filter-jobs">
<blockquote> <div class="input-append">
[% IF activeJobs.size == 0 %]<em>(none)</em>[% END %] <input name="filter" type="text" class="input-large search-query" placeholder="Search jobs by name..." [% HTML.attributes(value => filter) %]/>
[% FOREACH j IN activeJobs %][% INCLUDE renderJobName project=project.name jobset=jobset.name job=j %]<br/>[% END %] <button type="button" class="btn btn-info [% IF showInactive %]active[% END %]" id="active-toggle">Show inactive jobs</button>
</blockquote> </div>
</p> &nbsp;
<img src="[% c.uri_for("/static/images/ajax-loader.gif") %]" alt="Loading..." style="display: none;" id="filter-loading" />
</form>
<p>This jobset used to contain the following [% inactiveJobs.size %] jobs: <script>
<blockquote> function setFilter(filter) {
[% IF inactiveJobs.size == 0 %]<em>(none)</em>[% END %] $('#filter-loading').show();
[% FOREACH j IN inactiveJobs %][% INCLUDE renderJobName project=project.name jobset=jobset.name job=j %]<br/>[% END %] if ($('#active-toggle').hasClass('active')) filter += '&amp;showInactive=1';
</blockquote> $('#tabs-jobs').load("[% c.uri_for('/jobset' project.name jobset.name "jobs-tab") %]", filter, function(response, status, xhr) {
</p> if (status == "error") {
$('#[% tabName %]').html("<div class='alert alert-error'>Error loading tab: " + xhr.status + " " + xhr.statusText + "</div>");
}
});
};
$('#filter-jobs').submit(function() {
setFilter($('#filter-jobs').serialize());
return false;
});
$('#active-toggle').click(function() {
$(this).toggleClass('active');
$('#filter-jobs').submit();
});
</script>
[% IF jobs.size == 0 %]
<div class="alert">There are no matching jobs.</div>
[% ELSE %]
[% IF nrJobs > jobs.size %]
<div class="alert">Showing the first [% jobs.size %] jobs. <a href="javascript:setFilter('filter=%')">Show all [% nrJobs %] jobs...</a></div>
[% END %]
[% evalIds = evals.keys.nsort.reverse %]
<table class="table table-striped table-condensed table-header-rotated">
<thead>
<tr>
<th style="width: 1em;">Job</th>
[% FOREACH eval IN evalIds %]
<th class="rotate-45">
<div><span>
<a href="[% c.uri_for('/eval' eval) %]">[% eval %]</a>
</span></div></th>
[% END %]
</tr>
</thead>
<tbody>
[% FOREACH j IN jobs-%]
<tr>
<th><span [% IF inactiveJobs.$j %]class="muted override-link"[% END %]>[% INCLUDE renderJobName project=project.name jobset=jobset.name job=j %]</span></th>
[% FOREACH eval IN evalIds %]
<td>[% r = evals.$eval.$j; IF r.id %]<a href="[% c.uri_for('/build' r.id) %]">[% INCLUDE renderBuildStatusIcon size=16 build=r %]</a>[% END %]</td>
[% END %]
</tr>
[% END %]
</tbody>
</table>
[% END %]

View file

@ -1,23 +0,0 @@
[% PROCESS common.tt %]
<table class="table table-striped table-condensed">
<thead><tr><th>Job</th>[% FOREACH s IN systems %]<th>[% s.system %]</th>[% END %]</tr></thead>
<tbody>
[% FOREACH j IN activeJobsStatus %]
<tr>
<td>[% INCLUDE renderJobName project=project.name jobset = jobset.name job = j.get_column('job') %]</td>
[% FOREACH s IN systems %]
[% system = s.system %]
[% systemStatus = j.get_column(system) %]
<td class="centered">
[% IF systemStatus != undef %]
<a href="[% c.uri_for('/build' j.get_column(system _ '-build') ) %]">
[% INCLUDE renderBuildStatusIcon buildstatus=systemStatus size=16 %]
</a>
[% END %]
</td>
[% END %]
</tr>
[% END %]
</tbody>
</table>

View file

@ -1,5 +1,6 @@
[% WRAPPER layout.tt title="Jobset $project.name:$jobset.name" %] [% WRAPPER layout.tt title="Jobset $project.name:$jobset.name" %]
[% PROCESS common.tt %] [% PROCESS common.tt %]
[% USE format %]
[% BLOCK renderJobsetInput %] [% BLOCK renderJobsetInput %]
@ -11,7 +12,7 @@
[% INCLUDE renderSelection curValue=input.type param="$baseName-type" options=inputTypes %] [% INCLUDE renderSelection curValue=input.type param="$baseName-type" options=inputTypes %]
</td> </td>
<td class="inputalts" id="[% baseName %]"> <td class="inputalts" id="[% baseName %]">
[% FOREACH alt IN input.jobsetinputalts %] [% FOREACH alt IN input.search_related('jobsetinputalts', {}, { order_by => 'altnr' }) %]
<tt class="inputalt"> <tt class="inputalt">
[% IF input.type == "string" %] [% IF input.type == "string" %]
"[% HTML.escape(alt.value) %]" "[% HTML.escape(alt.value) %]"
@ -41,11 +42,25 @@
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="active"><a href="#tabs-evaluations" data-toggle="tab">Evaluations</a></li> [% IF c.user_exists %]
[% IF jobset.errormsg %] <li class="dropdown">
<li><a href="#tabs-errors" data-toggle="tab"><img src="/static/images/error_16.png" /> Evaluation errors</a></li> <a class="dropdown-toggle" data-toggle="dropdown" href="#">
Actions
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
[% INCLUDE menuItem title="Edit configuration" icon="icon-edit" uri=c.uri_for(c.controller('Jobset').action_for('edit'), c.req.captures) %]
[% INCLUDE menuItem title="Delete this jobset" icon="icon-trash" uri="javascript:deleteJobset()" %]
[% INCLUDE menuItem title="Clone this jobset" uri=c.uri_for(c.controller('Jobset').action_for('edit'), c.req.captures, { clone => 1 }) %]
[% INCLUDE menuItem title="Evaluate this jobset" uri="javascript:confirmEvaluateJobset()" %]
</ul>
</li>
[% END %]
<li class="active"><a href="#tabs-evaluations" data-toggle="tab">Evaluations</a></li>
[% IF jobset.errormsg || jobset.fetcherrormsg %]
<li><a href="#tabs-errors" data-toggle="tab"><span class="text-warning">Evaluation errors</span></a></li>
[% END %] [% END %]
<li><a href="#tabs-status" data-toggle="tab">Job status</a></li>
<li><a href="#tabs-jobs" data-toggle="tab">Jobs</a></li> <li><a href="#tabs-jobs" data-toggle="tab">Jobs</a></li>
<li><a href="#tabs-configuration" data-toggle="tab">Configuration</a></li> <li><a href="#tabs-configuration" data-toggle="tab">Configuration</a></li>
</ul> </ul>
@ -59,7 +74,7 @@
<th>Last checked:</th> <th>Last checked:</th>
<td> <td>
[% IF jobset.lastcheckedtime %] [% IF jobset.lastcheckedtime %]
[% INCLUDE renderDateTime timestamp = jobset.lastcheckedtime %], [% IF jobset.errormsg %]<em class="text-warning">with errors!</em>[% ELSE %]<em>no errors</em>[% END %] [% INCLUDE renderDateTime timestamp = jobset.lastcheckedtime %], [% IF jobset.errormsg || jobset.fetcherrormsg %]<em class="text-warning">with errors!</em>[% ELSE %]<em>no errors</em>[% END %]
[% ELSE %] [% ELSE %]
<em>never</em> <em>never</em>
[% END %] [% END %]
@ -91,20 +106,20 @@
</div> </div>
[% INCLUDE makeLazyTab tabName="tabs-status" uri=c.uri_for('/jobset' project.name jobset.name "status-tab") %] [% IF jobset.errormsg || jobset.fetcherrormsg %]
[% IF jobset.errormsg %]
<div id="tabs-errors" class="tab-pane"> <div id="tabs-errors" class="tab-pane">
<p>Errors occurred at [% INCLUDE renderDateTime timestamp=jobset.errortime %].</p> <p>Errors occurred at [% INCLUDE renderDateTime timestamp=jobset.errortime %].</p>
<pre class="alert alert-error">[% HTML.escape(jobset.errormsg) %]</pre> <pre class="alert alert-error">[% HTML.escape(jobset.fetcherrormsg || jobset.errormsg) %]</pre>
</div> </div>
[% END %] [% END %]
<div id="tabs-configuration" class="tab-pane"> <div id="tabs-configuration" class="tab-pane">
<a class="btn pull-right" href="[% c.uri_for('/jobset' project.name jobset.name "edit") %]"><i class="icon-edit"></i> Edit</a>
<table class="info-table"> <table class="info-table">
<tr>
<th>State:</th>
<td>[% IF jobset.enabled == 0; "Disabled"; ELSIF jobset.enabled == 1; "Enabled"; ELSIF jobset.enabled == 2; "One-shot"; END %]</td>
</tr>
<tr> <tr>
<th>Description:</th> <th>Description:</th>
<td>[% HTML.escape(jobset.description) %]</td> <td>[% HTML.escape(jobset.description) %]</td>
@ -116,14 +131,14 @@
<tt>[% HTML.escape(jobset.nixexprinput) %]</tt> <tt>[% HTML.escape(jobset.nixexprinput) %]</tt>
</td> </td>
</tr> </tr>
<tr>
<th>Enabled:</th>
<td>[% jobset.enabled ? "Yes" : "No" %]</td>
</tr>
<tr> <tr>
<th>Check interval:</th> <th>Check interval:</th>
<td>[% jobset.checkinterval || "<em>disabled</em>" %]</td> <td>[% jobset.checkinterval || "<em>disabled</em>" %]</td>
</tr> </tr>
<tr>
<th>Scheduling shares:</th>
<td>[% jobset.schedulingshares %] [% IF totalShares %] ([% f = format("%.2f"); f(jobset.schedulingshares / totalShares * 100) %]% out of [% totalShares %] shares)[% END %]</td>
</tr>
<tr> <tr>
<th>Enable email notification:</th> <th>Enable email notification:</th>
<td>[% jobset.enableemail ? "Yes" : "No" %]</td> <td>[% jobset.enableemail ? "Yes" : "No" %]</td>
@ -133,7 +148,7 @@
<td>[% HTML.escape(jobset.emailoverride) %]</td> <td>[% HTML.escape(jobset.emailoverride) %]</td>
</tr> </tr>
<tr> <tr>
<th>Number of builds to keep:</th> <th>Number of evaluations to keep:</th>
<td>[% jobset.keepnr %]</td> <td>[% jobset.keepnr %]</td>
</tr> </tr>
</table> </table>
@ -145,4 +160,32 @@
</div> </div>
<script>
function confirmEvaluateJobset() {
bootbox.confirm(
'Are you sure you want to force evaluation of this jobset?',
function(c) {
if (!c) return;
requestJSON({
url: "[% HTML.escape(c.uri_for('/api/push', { jobsets = project.name _ ':' _ jobset.name, force = "1" })) %]",
success: function(data) {
bootbox.alert("The jobset has been scheduled for evaluation.");
}
});
});
};
function deleteJobset() {
bootbox.confirm(
'Are you sure you want to delete this jobset?',
function(c) {
if (!c) return;
redirectJSON({
url: "[% c.uri_for(c.controller('Jobset').action_for('jobset'), c.req.captures) %]",
type: 'DELETE'
});
});
};
</script>
[% END %] [% END %]

View file

@ -1,15 +0,0 @@
[% WRAPPER layout.tt title="Job status" _
(job ? " of job $project.name:$jobset.name:$job.name" :
jobset ? " of jobset $project.name:$jobset.name" :
project ? " of project $project.name" : "") %]
[% PROCESS common.tt %]
<p>Below are the latest builds for each job. It is ordered by the status
change time (the timestamp of the last build that had a different
build result status). That is, it shows the jobs that most recently
changed from failed to successful or vice versa first.</p>
[% INCLUDE renderBuildList builds=latestBuilds showStatusChange=1
hideProjectName=project hideJobsetName=jobset hideJobName=job %]
[% END %]

View file

@ -1,9 +1,6 @@
[% USE date %] [% USE date %]
[% USE HTML %] [% USE HTML %]
<?xml version="1.0" encoding="UTF-8"?>
[% PROCESS common.tt %] [% PROCESS common.tt %]
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -19,24 +16,25 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="text/javascript" src="/static/bootstrap/js/bootstrap.min.js"></script> <script type="text/javascript" src="[% c.uri_for("/static/bootstrap/js/bootstrap.min.js") %]"></script>
<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet" /> <link href="[% c.uri_for("/static/bootstrap/css/bootstrap.min.css") %]" rel="stylesheet" />
<!-- hydra.css must be included before bootstrap-responsive to <!-- hydra.css must be included before bootstrap-responsive to
make the @media rule work. --> make the @media rule work. -->
<link rel="stylesheet" href="/static/css/hydra.css" type="text/css" /> <link rel="stylesheet" href="[% c.uri_for("/static/css/hydra.css") %]" type="text/css" />
<link href="/static/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet" /> <link rel="stylesheet" href="[% c.uri_for("/static/css/rotated-th.css") %]" type="text/css" />
<link href="[% c.uri_for("/static/bootstrap/css/bootstrap-responsive.min.css") %]" rel="stylesheet" />
<style> <style>
.popover { max-width: 40%; } .popover { max-width: 40%; }
</style> </style>
<script type="text/javascript" src="/static/js/bootbox.min.js"></script> <script type="text/javascript" src="[% c.uri_for("/static/js/bootbox.min.js") %]"></script>
<link rel="stylesheet" href="/static/css/tree.css" type="text/css" /> <link rel="stylesheet" href="[% c.uri_for("/static/css/tree.css") %]" type="text/css" />
<link rel="stylesheet" href="/static/css/logfile.css" type="text/css" /> <link rel="stylesheet" href="[% c.uri_for("/static/css/logfile.css") %]" type="text/css" />
<script type="text/javascript" src="/static/js/common.js"></script> <script type="text/javascript" src="[% c.uri_for("/static/js/common.js") %]"></script>
[% tracker %] [% tracker %]
@ -68,24 +66,27 @@
<div class="container"> <div class="container">
[% IF !hideHeader %]
<div class="page-header">
<h1><small>[% HTML.escape(title) %]</small></h1>
</div>
[% ELSE %]
<br />
[% END %]
[% IF flashMsg %] [% IF flashMsg %]
<br />
<div class="alert alert-info">[% flashMsg %]</div> <div class="alert alert-info">[% flashMsg %]</div>
[% END %] [% END %]
[% IF successMsg %] [% IF successMsg %]
<br />
<div class="alert alert-success">[% successMsg %]</div> <div class="alert alert-success">[% successMsg %]</div>
[% END %] [% END %]
[% IF errorMsg %] [% IF errorMsg %]
<div class="alert alert-error">Error: [% errorMsg %]</div> <br />
<div class="alert alert-warning">Error: [% errorMsg %]</div>
[% END %]
[% IF !hideHeader %]
<div class="page-header">
<h1><small>[% IF c.user_exists && starUri; INCLUDE makeStar; " "; END; HTML.escape(title) %]</small></h1>
</div>
[% ELSE %]
<br />
[% END %] [% END %]
[% content %] [% content %]
@ -93,7 +94,7 @@
<footer class="navbar"> <footer class="navbar">
<hr /> <hr />
<small> <small>
<em><a href="http://nixos.org/hydra" target="_new">Hydra</a> [% HTML.escape(version) %] (using [% HTML.escape(nixVersion) %]).</em> <em><a href="http://nixos.org/hydra" target="_blank">Hydra</a> [% HTML.escape(version) %] (using [% HTML.escape(nixVersion) %]).</em>
[% IF c.user_exists %] [% IF c.user_exists %]
You are signed in as <tt>[% HTML.escape(c.user.username) %]</tt>. You are signed in as <tt>[% HTML.escape(c.user.username) %]</tt>.
[% END %] [% END %]

View file

@ -13,14 +13,14 @@ You are already signed in as <tt>[% HTML.escape(c.user.username) %]</tt>.
<div class="control-group"> <div class="control-group">
<label class="control-label">User name</label> <label class="control-label">User name</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="username" value=""></input> <input type="text" class="span3" name="username" value=""/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Password</label> <label class="control-label">Password</label>
<div class="controls"> <div class="controls">
<input type="password" class="span3" name="password" value=""></input> <input type="password" class="span3" name="password" value=""/>
</div> </div>
</div> </div>

View file

@ -40,7 +40,7 @@
<tr class="product"> <tr class="product">
<td> <td>
<a href="[% contents %]"> <a href="[% contents %]">
<img src="/static/images/error_32.png" alt="Source" /> <img src="[% c.uri_for("/static/images/error_32.png") %]" alt="Source" />
Failed build produced output. Click here to inspect the output. Failed build produced output. Click here to inspect the output.
</a> </a>
[% WRAPPER makePopover title="Help" classes="btn-mini" %] [% WRAPPER makePopover title="Help" classes="btn-mini" %]
@ -59,7 +59,7 @@
<td> <td>
[% uri = c.uri_for('/build' build.id 'nix' 'pkg' "${build.nixname}-${build.system}.nixpkg") %] [% uri = c.uri_for('/build' build.id 'nix' 'pkg' "${build.nixname}-${build.system}.nixpkg") %]
<a href="[% uri %]"> <a href="[% uri %]">
<img src="/static/images/nix-build.png" alt="Source" /> <img src="[% c.uri_for("/static/images/nix-build.png") %]" alt="Source" />
One-click install of Nix package <tt>[% build.nixname %]</tt> One-click install of Nix package <tt>[% build.nixname %]</tt>
</a> </a>
[% WRAPPER makePopover title="Help" classes="btn-mini" %] [% WRAPPER makePopover title="Help" classes="btn-mini" %]
@ -87,7 +87,7 @@
[% uri = c.uri_for('/build' build.id 'nix' 'closure' filename ) %] [% uri = c.uri_for('/build' build.id 'nix' 'closure' filename ) %]
<a href="[% uri %]"> <a href="[% uri %]">
<img src="/static/images/nix-build.png" alt="Source" /> <img src="[% c.uri_for("/static/images/nix-build.png") %]" alt="Source" />
Nix closure of path <tt>[% product.path %]</tt> Nix closure of path <tt>[% product.path %]</tt>
</a> </a>
@ -127,17 +127,17 @@
<a href="[% uri %]"> <a href="[% uri %]">
[% SWITCH product.subtype %] [% SWITCH product.subtype %]
[% CASE "source-dist" %] [% CASE "source-dist" %]
<img src="/static/images/source-dist.png" alt="Source" /> Source distribution <tt>[% product.name %]</tt> <img src="[% c.uri_for("/static/images/source-dist.png") %]" alt="Source" /> Source distribution <tt>[% product.name %]</tt>
[% CASE "rpm" %] [% CASE "rpm" %]
<img src="/static/images/rpm.png" alt="RPM" /> RPM package <tt>[% product.name %]</tt> <img src="[% c.uri_for("/static/images/rpm.png") %]" alt="RPM" /> RPM package <tt>[% product.name %]</tt>
[% CASE "srpm" %] [% CASE "srpm" %]
<img src="/static/images/rpm.png" alt="Source RPM" /> Source RPM package <tt>[% product.name %]</tt> <img src="[% c.uri_for("/static/images/rpm.png") %]" alt="Source RPM" /> Source RPM package <tt>[% product.name %]</tt>
[% CASE "deb" %] [% CASE "deb" %]
<img src="/static/images/debian.png" alt="RPM" /> Debian package <tt>[% product.name %]</tt> <img src="[% c.uri_for("/static/images/debian.png") %]" alt="RPM" /> Debian package <tt>[% product.name %]</tt>
[% CASE "iso" %] [% CASE "iso" %]
<img src="/static/images/iso.png" alt="ISO" /> ISO-9660 CD/DVD image <tt>[% product.name %]</tt> <img src="[% c.uri_for("/static/images/iso.png") %]" alt="ISO" /> ISO-9660 CD/DVD image <tt>[% product.name %]</tt>
[% CASE "binary-dist" %] [% CASE "binary-dist" %]
<img src="/static/images/binary-dist.png" alt="Binary distribution" /> Binary distribution <tt>[% product.name %]</tt> <img src="[% c.uri_for("/static/images/binary-dist.png") %]" alt="Binary distribution" /> Binary distribution <tt>[% product.name %]</tt>
[% CASE DEFAULT %] [% CASE DEFAULT %]
File <tt>[% product.name %]</tt> of type <tt>[% product.subtype %]</tt> File <tt>[% product.name %]</tt> of type <tt>[% product.subtype %]</tt>
[% END %] [% END %]
@ -160,7 +160,7 @@
<tr class="product"> <tr class="product">
<td> <td>
<a href="[% uri %]"> <a href="[% uri %]">
<img src="/static/images/report.png" alt="Report" /> <img src="[% c.uri_for("/static/images/report.png") %]" alt="Report" />
[% SWITCH product.subtype %] [% SWITCH product.subtype %]
[% CASE "coverage" %] [% CASE "coverage" %]
Code coverage analysis report Code coverage analysis report
@ -177,9 +177,9 @@
<td> <td>
<a href="[% uri %]"> <a href="[% uri %]">
[% IF product.type == "doc-pdf" %] [% IF product.type == "doc-pdf" %]
<img src="/static/images/pdf.png" alt="PDF document" /> <img src="[% c.uri_for("/static/images/pdf.png") %]" alt="PDF document" />
[% ELSE %] [% ELSE %]
<img src="/static/images/document.png" alt="Document" /> <img src="[% c.uri_for("/static/images/document.png") %]" alt="Document" />
[% END %] [% END %]
[% SWITCH product.subtype %] [% SWITCH product.subtype %]
[% CASE "readme" %] [% CASE "readme" %]

View file

@ -2,16 +2,33 @@
[% PROCESS common.tt %] [% PROCESS common.tt %]
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
[% IF c.user_exists %]
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
Actions
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
[% INCLUDE menuItem title="Edit configuration" icon="icon-edit" uri=c.uri_for(c.controller('Project').action_for('edit'), c.req.captures) %]
[% INCLUDE menuItem title="Delete this project" icon="icon-trash" uri="javascript:deleteProject()" %]
[% INCLUDE menuItem title="Create jobset" icon="icon-plus" uri=c.uri_for(c.controller('Project').action_for('create_jobset'), c.req.captures) %]
[% INCLUDE menuItem title="Create release" icon="icon-plus" uri=c.uri_for(c.controller('Project').action_for('create_release'), c.req.captures) %]
</ul>
</li>
[% END %]
<li class="active"><a href="#tabs-project" data-toggle="tab">Jobsets</a></li> <li class="active"><a href="#tabs-project" data-toggle="tab">Jobsets</a></li>
<li><a href="#tabs-configuration" data-toggle="tab">Configuration</a></li> <li><a href="#tabs-configuration" data-toggle="tab">Configuration</a></li>
<li><a href="#tabs-releases" data-toggle="tab">Releases</a></li> <li><a href="#tabs-releases" data-toggle="tab">Releases</a></li>
[% IF views.size > 0 %]
<li><a href="#tabs-views" data-toggle="tab">Views</a></li> <li><a href="#tabs-views" data-toggle="tab">Views</a></li>
[% END %]
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div id="tabs-project" class="tab-pane active"> <div id="tabs-project" class="tab-pane active">
[% IF project.jobsets.size > 0 %] [% IF project.jobsets %]
<p>This project has the following jobsets:</p> <p>This project has the following jobsets:</p>
<table class="table table-striped table-condensed clickable-rows"> <table class="table table-striped table-condensed clickable-rows">
@ -30,18 +47,18 @@
<tr> <tr>
<td> <td>
[% IF j.get_column('nrscheduled') > 0 %] [% IF j.get_column('nrscheduled') > 0 %]
<img src="/static/images/help_16.png" alt="Scheduled" /> <img src="[% c.uri_for("/static/images/help_16.png") %]" alt="Scheduled" />
[% ELSIF j.get_column('nrfailed') == 0 %] [% ELSIF j.get_column('nrfailed') == 0 %]
<img src="/static/images/checkmark_16.png" alt="Succeeded" /> <img src="[% c.uri_for("/static/images/checkmark_16.png") %]" alt="Succeeded" />
[% ELSIF j.get_column('nrfailed') > 0 && j.get_column('nrsucceeded') > 0 %] [% ELSIF j.get_column('nrfailed') > 0 && j.get_column('nrsucceeded') > 0 %]
<img src="/static/images/error_some_16.png" alt="Some Failed" /> <img src="[% c.uri_for("/static/images/error_some_16.png") %]" alt="Some Failed" />
[% ELSE %] [% ELSE %]
<img src="/static/images/error_16.png" alt="All Failed" /> <img src="[% c.uri_for("/static/images/error_16.png") %]" alt="All Failed" />
[% END %] [% END %]
</td> </td>
<td><span class="[% IF !j.enabled %]disabled-jobset[% END %] [%+ IF j.hidden %]hidden-jobset[% END %]">[% INCLUDE renderJobsetName project=project.name jobset=j.name inRow=1 %]</span></td> <td><span class="[% IF !j.enabled %]disabled-jobset[% END %] [%+ IF j.hidden %]hidden-jobset[% END %]">[% INCLUDE renderJobsetName project=project.name jobset=j.name inRow=1 %]</span></td>
<td>[% HTML.escape(j.description) %]</td> <td>[% HTML.escape(j.description) %]</td>
<td>[% INCLUDE renderDateTime timestamp = j.lastcheckedtime %]</td> <td>[% IF j.lastcheckedtime; INCLUDE renderDateTime timestamp = j.lastcheckedtime; ELSE; "-"; END %]</td>
[% IF j.get_column('nrtotal') > 0 %] [% IF j.get_column('nrtotal') > 0 %]
[% successrate = ( j.get_column('nrsucceeded') / j.get_column('nrtotal') )*100 %] [% successrate = ( j.get_column('nrsucceeded') / j.get_column('nrtotal') )*100 %]
[% IF j.get_column('nrscheduled') > 0 %] [% IF j.get_column('nrscheduled') > 0 %]
@ -78,7 +95,6 @@
</div> </div>
<div id="tabs-configuration" class="tab-pane"> <div id="tabs-configuration" class="tab-pane">
<a class="btn pull-right" href="[% c.uri_for('/project' project.name "edit") %]"><i class="icon-edit"></i> Edit</a>
<table class="info-table"> <table class="info-table">
<tr> <tr>
<th>Display name:</th> <th>Display name:</th>
@ -138,12 +154,6 @@
[% END %] [% END %]
[% IF c.user_exists %]
<p><a class="btn" href="[% c.uri_for('/project' project.name 'create-release') %]">
<i class="icon-plus"></i> Create a release
</a></p>
[% END %]
</div> </div>
<div id="tabs-views" class="tab-pane"> <div id="tabs-views" class="tab-pane">
@ -176,4 +186,18 @@
</div> </div>
<script>
function deleteProject() {
bootbox.confirm(
'Are you sure you want to delete this project?',
function(c) {
if (!c) return;
redirectJSON({
url: "[% c.uri_for('/project' project.name) %]",
type: 'DELETE'
});
});
};
</script>
[% END %] [% END %]

View file

@ -141,11 +141,14 @@ fi
args+=(--arg '[% input.name %]' "{ outPath = $inputDir; rev = \"[% input.revision %]\"; }") args+=(--arg '[% input.name %]' "{ outPath = $inputDir; rev = \"[% input.revision %]\"; }")
[% ELSIF input.type == "string" %] [% ELSIF input.type == "string" %]
args+=(--arg '[% input.name %]' '"[% input.value %]"') args+=(--arg '[% input.name %]' '"[% input.value %]"') # FIXME: escape
[% ELSIF input.type == "boolean" %] [% ELSIF input.type == "boolean" %]
args+=(--arg '[% input.name %]' '[% input.value %]') args+=(--arg '[% input.name %]' '[% input.value %]')
[% ELSIF input.type == "nix" %]
args+=(--arg '[% input.name %]' '[% input.value %]') # FIXME: escape
[% ELSE %] [% ELSE %]
echo "$0: input [% input.name %] has unsupported type [% input.type %]" >&2 echo "$0: input [% input.name %] has unsupported type [% input.type %]" >&2
exit 1 exit 1

View file

@ -91,3 +91,20 @@ div.news-item:not(:first-child) {
td.nowrap { td.nowrap {
white-space: nowrap; white-space: nowrap;
} }
.override-link a {
color: inherit;
}
.actions {
font-weight: bold;
}
.star {
color: black;
font-size: 110%;
}
.star:hover {
cursor: pointer;
}

View file

@ -0,0 +1,52 @@
/* Rotated table headers, borrowed from http://jimmybonney.com/articles/column_header_rotation_css */
.tab-content {
margin-right: 5em;
overflow: visible;
}
td.centered {
text-align: center;
}
.table-header-rotated th.rotate-45{
height: 80px;
width: 40px;
min-width: 40px;
max-width: 40px;
position: relative;
vertical-align: bottom;
padding: 0;
font-size: 100%;
line-height: 0.9;
}
.table-header-rotated th.rotate-45 > div {
position: relative;
top: 0px;
left: 40px; /* 80 * tan(45) / 2 = 40 where 80 is the height on the cell and 45 is the transform angle*/
height: 100%;
-ms-transform:skew(-45deg,0deg);
-moz-transform:skew(-45deg,0deg);
-webkit-transform:skew(-45deg,0deg);
-o-transform:skew(-45deg,0deg);
transform:skew(-45deg,0deg);
overflow: hidden;
border-left: 1px solid #dddddd;
}
.table-header-rotated th.rotate-45 span {
-ms-transform:skew(45deg,0deg) rotate(315deg);
-moz-transform:skew(45deg,0deg) rotate(315deg);
-webkit-transform:skew(45deg,0deg) rotate(315deg);
-o-transform:skew(45deg,0deg) rotate(315deg);
transform:skew(45deg,0deg) rotate(315deg);
position: absolute;
bottom: 30px; /* 40 cos(45) = 28 with an additional 2px margin*/
left: -25px; /*Because it looked good, but there is probably a mathematical link here as well*/
display: inline-block;
// width: 100%;
width: 85px; /* 80 / cos(45) - 40 cos (45) = 85 where 80 is the height of the cell, 40 the width of the cell and 45 the transform angle*/
text-align: left;
// white-space: nowrap; /*whether to display in one line or not*/
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

View file

@ -50,6 +50,17 @@ $(document).ready(function() {
$(".hydra-popover").popover({}); $(".hydra-popover").popover({});
$(function() {
if (window.location.hash) {
$(".nav-tabs a[href='" + window.location.hash + "']").tab('show');
}
/* If no tab is active, show the first one. */
$(".nav-tabs").each(function() {
if ($("li.active", this).length > 0) return;
$("a", $(this).children("li:not(.dropdown)").first()).tab('show');
});
/* Ensure that pressing the back button on another page /* Ensure that pressing the back button on another page
navigates back to the previously selected tab on this navigates back to the previously selected tab on this
page. */ page. */
@ -58,10 +69,79 @@ $(document).ready(function() {
var id = e.target.toString().match(pattern)[0]; var id = e.target.toString().match(pattern)[0];
history.replaceState(null, "", id); history.replaceState(null, "", id);
}); });
});
$(function() { /* Automatically set Bootstrap radio buttons from hidden form controls. */
if (window.location.hash) { $('div[data-toggle="buttons-radio"] input[type="hidden"]').map(function(){
$(".nav a[href='" + window.location.hash + "']").tab('show'); $('button[value="' + $(this).val() + '"]', $(this).parent()).addClass('active');
});
/* Automatically update hidden form controls from Bootstrap radio buttons. */
$('div[data-toggle="buttons-radio"] .btn').click(function(){
$('input', $(this).parent()).val($(this).val());
});
$(".star").click(function(event) {
var star = $(this);
var active = star.text() != '★';
requestJSON({
url: star.attr("data-post"),
data: active ? "star=1" : "star=0",
type: 'POST',
success: function(res) {
if (active) {
star.text('★');
} else {
star.text('☆');
} }
}) }
});
});
}); });
var tabsLoaded = {};
function makeLazyTab(tabName, uri) {
$('.nav-tabs').bind('show', function(e) {
var pattern = /#.+/gi;
var id = e.target.toString().match(pattern)[0];
if (id == '#' + tabName && !tabsLoaded[id]) {
tabsLoaded[id] = 1;
$('#' + tabName).load(uri, function(response, status, xhr) {
if (status == "error") {
$('#' + tabName).html("<div class='alert alert-error'>Error loading tab: " + xhr.status + " " + xhr.statusText + "</div>");
}
});
}
});
};
function escapeHTML(s) {
return $('<div/>').text(s).html();
};
function requestJSON(args) {
args.dataType = 'json';
args.error = function(data) {
json = {};
try {
if (data.responseText)
json = $.parseJSON(data.responseText);
} catch (err) {
}
if (json.error)
bootbox.alert(escapeHTML(json.error));
else if (data.responseText)
bootbox.alert("Server error: " + escapeHTML(data.responseText));
else
bootbox.alert("Unknown server error!");
};
return $.ajax(args);
};
function redirectJSON(args) {
args.success = function(data) {
window.location = data.redirect;
};
return requestJSON(args);
};

View file

@ -1,19 +1,17 @@
[% BLOCK menuItem %]
<li class="[% IF "${root}${curUri}" == uri %]active[% END %]" [% IF confirmmsg %]onclick="javascript:return confirm('[% confirmmsg %]')"[% END %]>
<a [% HTML.attributes(href => uri) %]>[% title %]</a>
</li>
[% END %]
[% BLOCK makeSubMenu %] [% BLOCK makeSubMenu %]
<li class="dropdown"> <li class="dropdown">
<a class="dropdown-toggle" href="#" data-toggle="dropdown">[% title %]<b class="caret"></b></a> <a class="dropdown-toggle" href="#" data-toggle="dropdown">[% title %]<b class="caret"></b></a>
<ul id="left-menu" class="dropdown-menu"> <ul class="dropdown-menu">
[% content %] [% content %]
</ul> </ul>
</li> </li>
[% END %] [% END %]
<ul class="nav pull-left" id="top-menu"> <ul class="nav pull-left">
[% IF c.user_exists %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('User').action_for('dashboard'), [c.user.username]) title = "Dashboard" %]
[% END %]
[% WRAPPER makeSubMenu title="Status" %] [% WRAPPER makeSubMenu title="Status" %]
[% INCLUDE menuItem [% INCLUDE menuItem
@ -39,15 +37,7 @@
<li class="divider"></li> <li class="divider"></li>
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('project'), [project.name]) title = "Overview" %] [% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('project'), [project.name]) title = "Overview" %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('all'), [project.name]) title = "Latest builds" %] [% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('all'), [project.name]) title = "Latest builds" %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('jobstatus'), [project.name]) title = "Job status" %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('errors'), [project.name]) title = "Errors" %]
<li class="divider"></li>
[% INCLUDE menuItem uri = c.uri_for('/project' project.name 'channel' 'latest') title = "Channel" %] [% INCLUDE menuItem uri = c.uri_for('/project' project.name 'channel' 'latest') title = "Channel" %]
[% IF c.user_exists %]
<li class="divider"></li>
[% INCLUDE menuItem uri = c.uri_for('/project' project.name 'edit') title="Edit" %]
[% INCLUDE menuItem uri = c.uri_for(c.controller('Project').action_for('create_jobset'), [project.name]) title = "Create jobset" %]
[% END %]
[% END %] [% END %]
[% END %] [% END %]
@ -64,40 +54,7 @@
[% INCLUDE menuItem [% INCLUDE menuItem
uri = c.uri_for(c.controller('Jobset').action_for('all'), [project.name, jobset.name]) uri = c.uri_for(c.controller('Jobset').action_for('all'), [project.name, jobset.name])
title = "Latest builds" %] title = "Latest builds" %]
[% INCLUDE menuItem
uri = c.uri_for(c.controller('Jobset').action_for('jobstatus'), [project.name, jobset.name])
title = "Job status" %]
[% INCLUDE menuItem
uri = c.uri_for(c.controller('Jobset').action_for('errors'), [project.name, jobset.name])
title = "Errors" %]
<li class="divider"></li>
[% INCLUDE menuItem uri = c.uri_for('/jobset' project.name jobset.name 'channel' 'latest') title = "Channel" %] [% INCLUDE menuItem uri = c.uri_for('/jobset' project.name jobset.name 'channel' 'latest') title = "Channel" %]
[% IF c.user_exists %]
<li class="divider"></li>
[% INCLUDE menuItem uri = c.uri_for('/jobset' project.name jobset.name 'edit') title="Edit" %]
[% INCLUDE menuItem uri = c.uri_for('/jobset' project.name jobset.name 'clone') title="Clone" %]
<script>
function confirmEvaluateJobset() {
bootbox.confirm(
'Are you sure you want to force evaluation of this jobset?',
function(c) {
if (!c) return;
$.post("[% c.uri_for('/api/push', { jobsets = project.name _ ':' _ jobset.name, force = "1" }) %]")
.done(function(data) {
if (data.error)
bootbox.alert("Unable to schedule the jobset for evaluation: " + data.error);
else
bootbox.alert("The jobset has been scheduled for evaluation.");
})
.fail(function() { bootbox.alert("Server request failed!"); });
});
return;
};
</script>
[% INCLUDE menuItem title="Evaluate" uri = "javascript:confirmEvaluateJobset()" %]
[% END %]
[% END %] [% END %]
[% END %] [% END %]
@ -111,54 +68,10 @@
[% INCLUDE menuItem [% INCLUDE menuItem
uri = c.uri_for(c.controller('Job').action_for('all'), [project.name, jobset.name, job.name]) uri = c.uri_for(c.controller('Job').action_for('all'), [project.name, jobset.name, job.name])
title = "Latest builds" %] title = "Latest builds" %]
[% INCLUDE menuItem
uri = c.uri_for(c.controller('Job').action_for('jobstatus'), [project.name, jobset.name, job.name])
title = "Job status" %]
[% INCLUDE menuItem
uri = c.uri_for(c.controller('Job').action_for('errors'), [project.name, jobset.name, job.name])
title = "Errors" %]
<li class="divider"></li>
[% INCLUDE menuItem uri = c.uri_for('/job' project.name jobset.name job.name 'channel' 'latest') title = "Channel" %] [% INCLUDE menuItem uri = c.uri_for('/job' project.name jobset.name job.name 'channel' 'latest') title = "Channel" %]
[% END %] [% END %]
[% END %] [% END %]
[% IF build %]
[% WRAPPER makeSubMenu title="Build" %]
<li class="nav-header">#[% build.id %]</li>
<li class="divider"></li>
[% INCLUDE menuItem
uri = c.uri_for('/build' build.id)
title = "Overview" %]
[% IF c.user_exists %]
<li class="divider"></li>
[% INCLUDE menuItem
uri = c.uri_for('/build' build.id 'clone')
title = "Clone build" %]
[% IF available %]
[% IF build.keep %]
[% INCLUDE menuItem
uri = c.uri_for('/build' build.id 'keep' 0)
title = "Unkeep build" %]
[% ELSE %]
[% INCLUDE menuItem
uri = c.uri_for('/build' build.id 'keep' 1)
title = "Keep build" %]
[% END %]
[% END %]
[% IF build.finished %]
[% INCLUDE menuItem
uri = c.uri_for('/build' build.id 'restart')
title = "Restart build" %]
[% END %]
[% IF !build.finished %]
[% INCLUDE menuItem
uri = c.uri_for('/build' build.id 'cancel')
title = "Cancel build" %]
[% END %]
[% END %]
[% END %]
[% END %]
[% IF c.user_exists && c.check_user_roles('admin') %] [% IF c.user_exists && c.check_user_roles('admin') %]
[% WRAPPER makeSubMenu title="Admin" %] [% WRAPPER makeSubMenu title="Admin" %]
[% IF c.check_user_roles('admin') %] [% IF c.check_user_roles('admin') %]
@ -182,18 +95,23 @@
class = "" %] class = "" %]
[% INCLUDE menuItem [% INCLUDE menuItem
uri = c.uri_for(c.controller('Admin').action_for('clear_queue_non_current')) uri = c.uri_for(c.controller('Admin').action_for('clear_queue_non_current'))
title = "Clear all non-running old builds from queue" title = "Clear scheduled non-current builds from queue"
confirmmsg = "Are you sure you want to clear the queue?" confirmmsg = "Are you sure you want to clear the queue?"
class = "" %] class = "" %]
[% INCLUDE menuItem
uri = c.uri_for(c.controller('Admin').action_for('clearvcscache'))
title = "Clear VCS caches"
confirmmsg = "Are you sure you want to clear the VCS cache?"
class = "" %]
[% END %] [% END %]
[% END %] [% END %]
</ul> </ul>
<ul class="nav pull-right" id="top-menu"> <ul class="nav pull-right">
<form class="navbar-search" action="[% c.uri_for('/search') %]"> <form class="navbar-search" action="[% c.uri_for('/search') %]">
<input name="query" type="text" class="search-query span2" placeholder="Search" [% HTML.attributes(value => c.req.params.query) %]></input> <input name="query" type="text" class="search-query span2" placeholder="Search" [% HTML.attributes(value => c.req.params.query) %]/>
</form> </form>
[% IF c.user_exists %] [% IF c.user_exists %]

View file

@ -9,7 +9,7 @@
[% BREAK IF checked %] [% BREAK IF checked %]
[% END %] [% END %]
[% IF checked %] [% IF checked %]
SELECTED selected="selected"
[% END %] [% END %]
>[% role %]</option> >[% role %]</option>
[% END %] [% END %]
@ -22,7 +22,7 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">User name</label> <label class="control-label">User name</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="username" [% HTML.attributes(value => username) %]></input> <input type="text" class="span3" name="username" [% HTML.attributes(value => username) %]/>
</div> </div>
</div> </div>
[% END %] [% END %]
@ -30,7 +30,7 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">Full name</label> <label class="control-label">Full name</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="fullname" [% HTML.attributes(value => fullname) %]></input> <input type="text" class="span3" name="fullname" [% HTML.attributes(value => fullname) %]/>
</div> </div>
</div> </div>
@ -38,14 +38,14 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">Password</label> <label class="control-label">Password</label>
<div class="controls"> <div class="controls">
<input type="password" class="span3" name="password" value=""></input> <input type="password" class="span3" name="password" value=""/>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">Confirm password</label> <label class="control-label">Confirm password</label>
<div class="controls"> <div class="controls">
<input type="password" class="span3" name="password2" value=""></input> <input type="password" class="span3" name="password2" value=""/>
</div> </div>
</div> </div>
[% END %] [% END %]
@ -54,7 +54,7 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">Email</label> <label class="control-label">Email</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="emailaddress" [% HTML.attributes(value => user.emailaddress) %]></input> <input type="text" class="span3" name="emailaddress" [% HTML.attributes(value => user.emailaddress) %]/>
</div> </div>
</div> </div>
--> -->
@ -63,7 +63,7 @@
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="emailonerror" [% IF emailonerror; 'checked="checked"'; END %]></input>Receive evaluation error notifications <input type="checkbox" name="emailonerror" [% IF emailonerror; 'checked="checked"'; END %]/>Receive evaluation error notifications
</label> </label>
</div> </div>
</div> </div>
@ -73,7 +73,7 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">Roles</label> <label class="control-label">Roles</label>
<div class="controls"> <div class="controls">
<select multiple name="roles" class="span3" [% IF !c.check_user_roles('admin') %]disabled="1"[% END %]> <select multiple="multiple" name="roles" class="span3" [% IF !c.check_user_roles('admin') %]disabled="1"[% END %]>
[% INCLUDE roleoption role="admin" %] [% INCLUDE roleoption role="admin" %]
[% INCLUDE roleoption role="create-project" %] [% INCLUDE roleoption role="create-project" %]
</select> </select>
@ -91,7 +91,7 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">Type the digits shown in the image above</label> <label class="control-label">Type the digits shown in the image above</label>
<div class="controls"> <div class="controls">
<input type="text" class="span3" name="captcha" value=""></input> <input type="text" class="span3" name="captcha" value=""/>
</div> </div>
</div> </div>
[% END %] [% END %]
@ -137,7 +137,8 @@
</script> </script>
[% END %] [% END %]
</div> </div>
</p>
</fieldset>
</form> </form>

View file

@ -27,11 +27,11 @@
<tr> <tr>
<td> <td>
[% IF result.status == 0 %] [% IF result.status == 0 %]
<img src="/static/images/checkmark_16.png" /> <img src="[% c.uri_for("/static/images/checkmark_16.png") %]" />
[% ELSIF result.status == 1 %] [% ELSIF result.status == 1 %]
<img src="/static/images/error_16.png" /> <img src="[% c.uri_for("/static/images/error_16.png") %]" />
[% ELSIF result.status == 2 %] [% ELSIF result.status == 2 %]
<img src="/static/images/help_16.png" /> <img src="[% c.uri_for("/static/images/help_16.png") %]" />
[% END %] [% END %]
</td> </td>
<td><a class="row-link" href="[% c.uri_for('/view' project.name view.name result.id) %]">[% result.id %]</a></td> <td><a class="row-link" href="[% c.uri_for('/view' project.name view.name result.id) %]">[% result.id %]</a></td>
@ -48,9 +48,9 @@
[% IF j.build %] [% IF j.build %]
<a href="[% c.uri_for('/build' j.build.id) %]"> <a href="[% c.uri_for('/build' j.build.id) %]">
[% IF j.build.get_column('buildstatus') == 0 %] [% IF j.build.get_column('buildstatus') == 0 %]
<img src="/static/images/checkmark_16.png" /> <img src="[% c.uri_for("/static/images/checkmark_16.png") %]" />
[% ELSE %] [% ELSE %]
<img src="/static/images/error_16.png" /> <img src="[% c.uri_for("/static/images/error_16.png") %]" />
[% END %] [% END %]
</a> </a>
[% END %] [% END %]

View file

@ -10,6 +10,7 @@ distributable_scripts = \
hydra-queue-runner \ hydra-queue-runner \
hydra-server \ hydra-server \
hydra-update-gc-roots \ hydra-update-gc-roots \
hydra-s3-backup-collect-garbage \
nix-prefetch-git \ nix-prefetch-git \
nix-prefetch-bzr \ nix-prefetch-bzr \
nix-prefetch-hg nix-prefetch-hg

View file

@ -8,8 +8,10 @@ use Nix::Store;
use Hydra::Plugin; use Hydra::Plugin;
use Hydra::Schema; use Hydra::Schema;
use Hydra::Helper::Nix; use Hydra::Helper::Nix;
use Hydra::Helper::PluginHooks;
use Hydra::Model::DB; use Hydra::Model::DB;
use Hydra::Helper::AddBuilds; use Hydra::Helper::AddBuilds;
use Set::Scalar;
STDOUT->autoflush(); STDOUT->autoflush();
@ -40,16 +42,18 @@ sub failDependents {
my ($drvPath, $status, $errorMsg, $dependents) = @_; my ($drvPath, $status, $errorMsg, $dependents) = @_;
# Get the referrer closure of $drvPath. # Get the referrer closure of $drvPath.
my @dependentDrvs = computeFSClosure(1, 0, $drvPath); my $dependentDrvs = Set::Scalar->new(computeFSClosure(1, 0, $drvPath));
my $time = time(); my $time = time();
txn_do($db, sub { txn_do($db, sub {
my @dependentBuilds = $db->resultset('Builds')->search( my @dependentBuilds = $db->resultset('Builds')->search(
{ drvpath => [ @dependentDrvs ], finished => 0, busy => 0 }); { finished => 0, busy => 0 },
{ columns => ["id", "project", "jobset", "job", "drvpath", "finished", "busy"] });
for my $d (@dependentBuilds) { for my $d (@dependentBuilds) {
next unless $dependentDrvs->has($d->drvpath);
print STDERR "failing dependent build ", $d->id, " of ", $d->project->name, ":", $d->jobset->name, ":", $d->job->name, "\n"; print STDERR "failing dependent build ", $d->id, " of ", $d->project->name, ":", $d->jobset->name, ":", $d->job->name, "\n";
$d->update( $d->update(
{ finished => 1 { finished => 1
@ -67,8 +71,8 @@ sub failDependents {
, drvpath => $drvPath , drvpath => $drvPath
, busy => 0 , busy => 0
, status => $status , status => $status
, starttime => time , starttime => $time
, stoptime => time , stoptime => $time
, errormsg => $errorMsg , errormsg => $errorMsg
}); });
addBuildStepOutputs($step); addBuildStepOutputs($step);
@ -80,19 +84,6 @@ sub failDependents {
} }
sub notify {
my ($build, $dependents) = @_;
foreach my $plugin (@plugins) {
eval {
$plugin->buildFinished($build, $dependents);
};
if ($@) {
print STDERR "$plugin->buildFinished: $@\n";
}
}
}
sub doBuild { sub doBuild {
my ($build) = @_; my ($build) = @_;
@ -132,7 +123,9 @@ sub doBuild {
# associated log files, etc. # associated log files, etc.
my $cmd = "nix-store --realise $drvPath " . my $cmd = "nix-store --realise $drvPath " .
"--timeout $timeout " . "--timeout $timeout " .
"--max-silent-time $maxsilent --keep-going --fallback " . "--max-silent-time $maxsilent " .
"--option build-max-log-size 67108864 " .
"--keep-going --fallback " .
"--no-build-output --log-type flat --print-build-trace " . "--no-build-output --log-type flat --print-build-trace " .
"--add-root " . gcRootFor($outputs{out} // $outputs{(sort keys %outputs)[0]}) . " 2>&1"; "--add-root " . gcRootFor($outputs{out} // $outputs{(sort keys %outputs)[0]}) . " 2>&1";
@ -149,6 +142,22 @@ sub doBuild {
next; next;
} }
# Hack to handle timeouts, which Nix doesn't report
# properly when they occur remotely. If we get a "hook
# failed" error and $maxsilent seconds have passed since
# the start of the build step, then assume that a timeout
# occured.
if (/^@\s+hook-failed\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/ && $3 eq "256") {
my $drvPathStep = $1;
if ($buildSteps{$drvPathStep}) {
my $step = $build->buildsteps->find({stepnr => $buildSteps{$drvPathStep}}) or die;
print STDERR $step->starttime, " ", time(), "\n";
if ($step->starttime + $maxsilent <= time) {
$_ = "@ build-failed $1 $2 timeout $4";
}
}
}
if (/^@\s+build-started\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/) { if (/^@\s+build-started\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/) {
my $drvPathStep = $1; my $drvPathStep = $1;
txn_do($db, sub { txn_do($db, sub {
@ -319,7 +328,7 @@ sub doBuild {
}); });
notify($build, $dependents); notifyBuildFinished(\@plugins, $build, $dependents);
} }
@ -328,7 +337,7 @@ print STDERR "performing build $buildId\n";
if ($ENV{'HYDRA_MAIL_TEST'}) { if ($ENV{'HYDRA_MAIL_TEST'}) {
my $build = $db->resultset('Builds')->find($buildId); my $build = $db->resultset('Builds')->find($buildId);
notify($build, []); notifyBuildFinished(\@plugins, $build, []);
exit 0; exit 0;
} }
@ -345,8 +354,8 @@ txn_do($db, sub {
die "build $buildId is already being built"; die "build $buildId is already being built";
} }
$build->update({busy => 1, locker => $$}); $build->update({busy => 1, locker => $$});
$build->buildsteps->search({busy => 1})->delete_all; $build->buildsteps->search({busy => 1})->delete;
$build->buildproducts->delete_all; $build->buildproducts->delete;
}); });
die unless $build; die unless $build;

View file

@ -2,6 +2,7 @@
use strict; use strict;
use feature 'switch'; use feature 'switch';
use utf8;
use Hydra::Schema; use Hydra::Schema;
use Hydra::Plugin; use Hydra::Plugin;
use Hydra::Helper::Nix; use Hydra::Helper::Nix;
@ -33,7 +34,7 @@ sub fetchInputs {
foreach my $input ($jobset->jobsetinputs->all) { foreach my $input ($jobset->jobsetinputs->all) {
foreach my $alt ($input->jobsetinputalts->all) { foreach my $alt ($input->jobsetinputalts->all) {
push @{$$inputInfo{$input->name}}, $_ push @{$$inputInfo{$input->name}}, $_
foreach fetchInput($plugins, $db, $project, $jobset, $input->name, $input->type, $alt->value); foreach fetchInput($plugins, $db, $project, $jobset, $input->name, $input->type, $alt->value, $input->emailresponsible);
} }
} }
} }
@ -41,12 +42,16 @@ sub fetchInputs {
sub setJobsetError { sub setJobsetError {
my ($jobset, $errorMsg) = @_; my ($jobset, $errorMsg) = @_;
my $prevError = $jobset->errormsg;
eval { eval {
txn_do($db, sub { txn_do($db, sub {
$jobset->update({errormsg => $errorMsg, errortime => time}); $jobset->update({ errormsg => $errorMsg, errortime => time, fetcherrormsg => undef });
}); });
}; };
if (defined $errorMsg && $errorMsg ne ($prevError // "")) {
sendJobsetErrorNotification($jobset, $errorMsg); sendJobsetErrorNotification($jobset, $errorMsg);
}
} }
@ -65,7 +70,7 @@ sub sendJobsetErrorNotification() {
my $body = "Hi,\n" my $body = "Hi,\n"
. "\n" . "\n"
. "This is to let you know that Hydra jobset evalation of $projectName:$jobsetName " . "This is to let you know that Hydra jobset evaluation of $projectName:$jobsetName "
. "resulted in the following error:\n" . "resulted in the following error:\n"
. "\n" . "\n"
. "$errorMsg" . "$errorMsg"
@ -110,7 +115,17 @@ sub checkJobsetWrapped {
# Fetch all values for all inputs. # Fetch all values for all inputs.
my $checkoutStart = time; my $checkoutStart = time;
eval {
fetchInputs($project, $jobset, $inputInfo); fetchInputs($project, $jobset, $inputInfo);
};
if ($@) {
my $msg = $@;
print STDERR $msg;
txn_do($db, sub {
$jobset->update({ lastcheckedtime => time, fetcherrormsg => $msg });
});
return;
}
my $checkoutStop = time; my $checkoutStop = time;
# Hash the arguments to hydra-eval-jobs and check the # Hash the arguments to hydra-eval-jobs and check the
@ -122,14 +137,14 @@ sub checkJobsetWrapped {
if (defined $prevEval && $prevEval->hash eq $argsHash) { if (defined $prevEval && $prevEval->hash eq $argsHash) {
print STDERR " jobset is unchanged, skipping\n"; print STDERR " jobset is unchanged, skipping\n";
txn_do($db, sub { txn_do($db, sub {
$jobset->update({lastcheckedtime => time}); $jobset->update({ lastcheckedtime => time, fetcherrormsg => undef });
}); });
return; return;
} }
# Evaluate the job expression. # Evaluate the job expression.
my $evalStart = time; my $evalStart = time;
my ($jobs, $nixExprInput) = evalJobs($inputInfo, $exprType, $jobset->nixexprinput, $jobset->nixexprpath); my ($jobs, $nixExprInput, $msg) = evalJobs($inputInfo, $exprType, $jobset->nixexprinput, $jobset->nixexprpath);
my $evalStop = time; my $evalStop = time;
my $jobOutPathMap = {}; my $jobOutPathMap = {};
@ -144,11 +159,11 @@ sub checkJobsetWrapped {
$jobset->builds->search({iscurrent => 1})->update({iscurrent => 0}); $jobset->builds->search({iscurrent => 1})->update({iscurrent => 0});
# Schedule each successfully evaluated job. # Schedule each successfully evaluated job.
my %buildIds; my %buildMap;
foreach my $job (permute @{$jobs->{job}}) { foreach my $job (permute @{$jobs->{job}}) {
next if $job->{jobName} eq ""; next if $job->{jobName} eq "";
print STDERR " considering job " . $project->name, ":", $jobset->name, ":", $job->{jobName} . "\n"; print STDERR " considering job " . $project->name, ":", $jobset->name, ":", $job->{jobName} . "\n";
checkBuild($db, $project, $jobset, $inputInfo, $nixExprInput, $job, \%buildIds, $prevEval, $jobOutPathMap); checkBuild($db, $jobset, $inputInfo, $nixExprInput, $job, \%buildMap, $prevEval, $jobOutPathMap, $plugins);
} }
# Update the last checked times and error messages for each # Update the last checked times and error messages for each
@ -161,23 +176,51 @@ sub checkJobsetWrapped {
$_->update({ errormsg => $failedJobNames{$_->name} ? join '\n', @{$failedJobNames{$_->name}} : undef }) $_->update({ errormsg => $failedJobNames{$_->name} ? join '\n', @{$failedJobNames{$_->name}} : undef })
foreach $jobset->jobs->all; foreach $jobset->jobs->all;
my $hasNewBuilds = 0; # Have any builds been added or removed since last time?
while (my ($id, $new) = each %buildIds) { my $jobsetChanged =
$hasNewBuilds = 1 if $new; (scalar(grep { $_->{new} } values(%buildMap)) > 0)
} || (defined $prevEval && $prevEval->jobsetevalmembers->count != scalar(keys %buildMap));
my $ev = $jobset->jobsetevals->create( my $ev = $jobset->jobsetevals->create(
{ hash => $argsHash { hash => $argsHash
, timestamp => time , timestamp => time
, checkouttime => abs($checkoutStop - $checkoutStart) , checkouttime => abs($checkoutStop - $checkoutStart)
, evaltime => abs($evalStop - $evalStart) , evaltime => abs($evalStop - $evalStart)
, hasnewbuilds => $hasNewBuilds , hasnewbuilds => $jobsetChanged ? 1 : 0
, nrbuilds => $hasNewBuilds ? scalar(keys %buildIds) : undef , nrbuilds => $jobsetChanged ? scalar(keys %buildMap) : undef
}); });
if ($hasNewBuilds) { if ($jobsetChanged) {
while (my ($id, $new) = each %buildIds) { # Create JobsetEvalMembers mappings.
$ev->jobsetevalmembers->create({ build => $id, isnew => $new }); while (my ($id, $x) = each %buildMap) {
$ev->jobsetevalmembers->create({ build => $id, isnew => $x->{new} });
}
# Create AggregateConstituents mappings. Since there can
# be jobs that alias each other, if there are multiple
# builds for the same derivation, pick the one with the
# shortest name.
my %drvPathToId;
while (my ($id, $x) = each %buildMap) {
my $y = $drvPathToId{$x->{drvPath}};
if (defined $y) {
next if length $x->{jobName} > length $y->{jobName};
next if length $x->{jobName} == length $y->{jobName} && $x->{jobName} ge $y->{jobName};
}
$drvPathToId{$x->{drvPath}} = $x;
}
foreach my $job (@{$jobs->{job}}) {
next unless $job->{constituents};
my $x = $drvPathToId{$job->{drvPath}} or die;
foreach my $drvPath (split / /, $job->{constituents}) {
my $constituent = $drvPathToId{$drvPath};
if (defined $constituent) {
$db->resultset('AggregateConstituents')->update_or_create({aggregate => $x->{id}, constituent => $constituent->{id}});
} else {
warn "aggregate job $job->{jobName} has a constituent $drvPath that doesn't correspond to a Hydra build\n";
}
}
} }
foreach my $name (keys %{$inputInfo}) { foreach my $name (keys %{$inputInfo}) {
@ -203,23 +246,16 @@ sub checkJobsetWrapped {
print STDERR " created cached eval ", $ev->id, "\n"; print STDERR " created cached eval ", $ev->id, "\n";
$prevEval->builds->update({iscurrent => 1}) if defined $prevEval; $prevEval->builds->update({iscurrent => 1}) if defined $prevEval;
} }
# If this is a one-shot jobset, disable it now.
$jobset->update({ enabled => 0 }) if $jobset->enabled == 2;
}); });
# Store the error messages for jobs that failed to evaluate. # Store the error messages for jobs that failed to evaluate.
my $msg = "";
foreach my $error (@{$jobs->{error}}) { foreach my $error (@{$jobs->{error}}) {
my $bindings = ""; $msg .=
foreach my $arg (@{$error->{arg}}) { ($error->{location} ne "" ? "in job $error->{location}" : "at top-level") .
my $input = $inputInfo->{$arg->{name}}->[$arg->{altnr}] or die "invalid input"; ":\n" . $error->{msg} . "\n\n";
$bindings .= ", " if $bindings ne "";
$bindings .= $arg->{name} . " = ";
given ($input->{type}) {
when ("string") { $bindings .= "\"" . $input->{value} . "\""; }
when ("boolean") { $bindings .= $input->{value}; }
default { $bindings .= "..."; }
}
}
$msg .= "at `" . $error->{location} . "' [$bindings]:\n" . $error->{msg} . "\n\n";
} }
setJobsetError($jobset, $msg); setJobsetError($jobset, $msg);
} }
@ -241,7 +277,7 @@ sub checkJobset {
if ($@) { if ($@) {
my $msg = $@; my $msg = $@;
print STDERR "error evaluating jobset ", $jobset->name, ": $msg"; print STDERR $msg;
txn_do($db, sub { txn_do($db, sub {
$jobset->update({lastcheckedtime => time}); $jobset->update({lastcheckedtime => time});
setJobsetError($jobset, $msg); setJobsetError($jobset, $msg);
@ -272,7 +308,7 @@ sub checkSomeJobset {
# longest time (but don't check more often than the jobset's # longest time (but don't check more often than the jobset's
# minimal check interval). # minimal check interval).
($jobset) = $db->resultset('Jobsets')->search( ($jobset) = $db->resultset('Jobsets')->search(
{ 'project.enabled' => 1, 'me.enabled' => 1, { 'project.enabled' => 1, 'me.enabled' => { '!=' => 0 },
, 'checkinterval' => { '!=', 0 } , 'checkinterval' => { '!=', 0 }
, -or => [ 'lastcheckedtime' => undef, 'lastcheckedtime' => { '<', \ (time() . " - me.checkinterval") } ] }, , -or => [ 'lastcheckedtime' => undef, 'lastcheckedtime' => { '<', \ (time() . " - me.checkinterval") } ] },
{ join => 'project', order_by => [ 'lastcheckedtime nulls first' ], rows => 1 }) { join => 'project', order_by => [ 'lastcheckedtime nulls first' ], rows => 1 })
@ -280,13 +316,10 @@ sub checkSomeJobset {
return 0 unless defined $jobset; return 0 unless defined $jobset;
checkJobset($jobset); return system($0, $jobset->project->name, $jobset->name) == 0;
return 1;
} }
# For testing: evaluate a single jobset, then exit.
if (scalar @ARGV == 2) { if (scalar @ARGV == 2) {
my $projectName = $ARGV[0]; my $projectName = $ARGV[0];
my $jobsetName = $ARGV[1]; my $jobsetName = $ARGV[1];

View file

@ -51,12 +51,12 @@ for (my $n = $schemaVersion; $n < $maxSchemaVersion; $n++) {
my @statements = $sql_splitter->split($schema); my @statements = $sql_splitter->split($schema);
eval { eval {
$dbh->begin_work; $dbh->begin_work;
sub run { sub run_ {
my ($stm) = @_; my ($stm) = @_;
print STDERR "executing SQL statement: $stm\n"; print STDERR "executing SQL statement: $stm\n";
$dbh->do($_); $dbh->do($_);
} }
run($_) foreach @statements; run_($_) foreach @statements;
$db->resultset('SchemaVersion')->update({version => $m}); $db->resultset('SchemaVersion')->update({version => $m});
$dbh->commit; $dbh->commit;
}; };

View file

@ -9,6 +9,7 @@ use Hydra::Helper::Nix;
use Hydra::Model::DB; use Hydra::Model::DB;
use IO::Handle; use IO::Handle;
use Nix::Store; use Nix::Store;
use Set::Scalar;
chdir Hydra::Model::DB::getHydraPath or die; chdir Hydra::Model::DB::getHydraPath or die;
my $db = Hydra::Model::DB->new(); my $db = Hydra::Model::DB->new();
@ -36,7 +37,7 @@ sub unlockDeadBuilds {
} }
if ($unlock) { if ($unlock) {
print "build ", $build->id, " pid $pid died, unlocking\n"; print "build ", $build->id, " pid $pid died, unlocking\n";
$build->update({ busy => 0, locker => ""}); $build->update({ busy => 0, locker => "" });
$build->buildsteps->search({ busy => 1 })->update({ busy => 0, status => 4, stoptime => time }); $build->buildsteps->search({ busy => 1 })->update({ busy => 0, status => 4, stoptime => time });
} }
} }
@ -52,14 +53,25 @@ sub findBuildDependencyInQueue {
my @deps = grep { /\.drv$/ && $_ ne $build->drvpath } computeFSClosure(0, 0, $build->drvpath); my @deps = grep { /\.drv$/ && $_ ne $build->drvpath } computeFSClosure(0, 0, $build->drvpath);
return unless scalar @deps > 0; return unless scalar @deps > 0;
foreach my $d (@deps) { foreach my $d (@deps) {
my $b = $buildsByDrv->{$d}; my $bs = $buildsByDrv->{$d};
next unless defined $b; next unless defined $bs;
return $db->resultset('Builds')->find($b); return $db->resultset('Builds')->find((@$bs)[0]);
} }
return undef; return undef;
} }
sub blockBuilds {
my ($buildsByDrv, $blockedBuilds, $build) = @_;
my @rdeps = grep { /\.drv$/ && $_ ne $build->drvpath } computeFSClosure(1, 0, $build->drvpath);
foreach my $drv (@rdeps) {
my $bs = $buildsByDrv->{$drv};
next if !defined $bs;
$blockedBuilds->insert($_) foreach @$bs;
}
}
sub checkBuilds { sub checkBuilds {
# print "looking for runnable builds...\n"; # print "looking for runnable builds...\n";
@ -70,27 +82,34 @@ sub checkBuilds {
my %maxConcurrent; my %maxConcurrent;
foreach my $machineName (keys %{$machines}) { foreach my $machineName (keys %{$machines}) {
foreach my $system (${$machines}{$machineName}{'systemTypes'}) { foreach my $system (@{${$machines}{$machineName}{'systemTypes'}}) {
$maxConcurrent{$system} = (${$machines}{$machineName}{'maxJobs'} or 0) + ($maxConcurrent{$system} or 0) $maxConcurrent{$system} = (${$machines}{$machineName}{'maxJobs'} or 0) + ($maxConcurrent{$system} or 0)
} }
} }
txn_do($db, sub { txn_do($db, sub {
# Cache scheduled by derivation path to speed up # Cache scheduled builds by derivation path to speed up
# findBuildDependencyInQueue. # findBuildDependencyInQueue.
my $buildsByDrv = {}; my $buildsByDrv = {};
$buildsByDrv->{$_->drvpath} = $_->id push @{$buildsByDrv->{$_->drvpath}}, $_->id
foreach $db->resultset('Builds')->search({ finished => 0, enabled => 1 }, { join => ['project'] }); foreach $db->resultset('Builds')->search({ finished => 0 });
# Builds in the queue of which a dependency is already building.
my $blockedBuilds = Set::Scalar->new();
blockBuilds($buildsByDrv, $blockedBuilds, $_)
foreach $db->resultset('Builds')->search({ finished => 0, busy => 1 });
# Get the system types for the runnable builds. # Get the system types for the runnable builds.
my @systemTypes = $db->resultset('Builds')->search( my @systemTypes = $db->resultset('Builds')->search(
{ finished => 0, busy => 0, enabled => 1 }, { finished => 0, busy => 0 },
{ join => ['project'], select => ['system'], as => ['system'], distinct => 1 }); { join => ['project'], select => ['system'], as => ['system'], distinct => 1 });
# Get the total number of scheduling shares.
my $totalShares = getTotalShares($db);
# For each system type, select up to the maximum number of # For each system type, select up to the maximum number of
# concurrent build for that system type. Choose the highest # concurrent build for that system type.
# priority builds first, then the oldest builds.
foreach my $system (@systemTypes) { foreach my $system (@systemTypes) {
# How many builds are already currently executing for this # How many builds are already currently executing for this
# system type? # system type?
@ -101,15 +120,67 @@ sub checkBuilds {
my $max = defined $systemTypeInfo ? $systemTypeInfo->maxconcurrent : $maxConcurrent{$system->system} // 2; my $max = defined $systemTypeInfo ? $systemTypeInfo->maxconcurrent : $maxConcurrent{$system->system} // 2;
my $extraAllowed = $max - $nrActive; my $extraAllowed = $max - $nrActive;
$extraAllowed = 0 if $extraAllowed < 0; next if $extraAllowed <= 0;
# Select the highest-priority builds to start. print STDERR "starting at most $extraAllowed builds for system ${\$system->system}\n";
my @builds = $extraAllowed == 0 ? () : $db->resultset('Builds')->search(
{ finished => 0, busy => 0, system => $system->system, enabled => 1 }, my $timeSpentPerJobset;
{ join => ['project'], order_by => ["priority DESC", "id"] });
j: while ($extraAllowed-- > 0) {
my @runnableJobsets = $db->resultset('Builds')->search(
{ finished => 0, busy => 0, system => $system->system },
{ select => ['project', 'jobset'], distinct => 1 });
next if @runnableJobsets == 0;
my $windowSize = 24 * 3600;
my $costPerBuild = 30;
my $totalWindowSize = $windowSize * $max;
my @res;
foreach my $b (@runnableJobsets) {
my $jobset = $db->resultset('Jobsets')->find($b->get_column('project'), $b->get_column('jobset')) or die;
my $timeSpent = $timeSpentPerJobset->{$b->get_column('project')}->{$b->get_column('jobset')};
if (!defined $timeSpent) {
$timeSpent = $jobset->builds->search(
{ },
{ where => \ ("(finished = 0 or (me.stoptime >= " . (time() - $windowSize) . "))")
, join => 'buildsteps'
, select => \ "sum(coalesce(buildsteps.stoptime, ${\time}) - buildsteps.starttime)"
, as => "sum" })->single->get_column("sum") // 0;
# Add a 30s penalty for each started build. This
# is to account for jobsets that have running
# builds but no build steps yet.
$timeSpent += $jobset->builds->search({ finished => 0, busy => 1 })->count * $costPerBuild;
$timeSpentPerJobset->{$b->get_column('project')}->{$b->get_column('jobset')} = $timeSpent;
}
my $share = $jobset->schedulingshares || 1; # prevent division by zero
my $used = $timeSpent / ($totalWindowSize * ($share / $totalShares));
#printf STDERR "%s:%s: %d s, total used = %.2f%%, share used = %.2f%%\n", $jobset->get_column('project'), $jobset->name, $timeSpent, $timeSpent / $totalWindowSize * 100, $used * 100;
push @res, { jobset => $jobset, used => $used };
}
foreach my $r (sort { $a->{used} <=> $b->{used} } @res) {
my $jobset = $r->{jobset};
#print STDERR "selected ", $jobset->get_column('project'), ':', $jobset->name, "\n";
# Select the highest-priority build for this jobset.
my @builds = $jobset->builds->search(
{ finished => 0, busy => 0, system => $system->system },
{ order_by => ["priority DESC", "id"] });
my $started = 0;
foreach my $build (@builds) { foreach my $build (@builds) {
next if $blockedBuilds->has($build->id);
# Find a dependency of $build that has no queued # Find a dependency of $build that has no queued
# dependencies itself. This isn't strictly necessary, # dependencies itself. This isn't strictly necessary,
# but it ensures that Nix builds are done as part of # but it ensures that Nix builds are done as part of
@ -120,6 +191,9 @@ sub checkBuilds {
} }
next if $build->busy; next if $build->busy;
printf STDERR "starting build %d (%s:%s:%s) on %s; jobset at %.2f%% of its share\n",
$build->id, $build->project->name, $build->jobset->name, $build->job->name, $build->system, $r->{used} * 100;
my $logfile = getcwd . "/logs/" . $build->id; my $logfile = getcwd . "/logs/" . $build->id;
mkdir(dirname $logfile); mkdir(dirname $logfile);
unlink($logfile); unlink($logfile);
@ -127,25 +201,30 @@ sub checkBuilds {
{ busy => 1 { busy => 1
, locker => $$ , locker => $$
, logfile => $logfile , logfile => $logfile
, starttime => time()
}); });
push @buildsStarted, $build; push @buildsStarted, $build;
last if ++$started >= $extraAllowed; $timeSpentPerJobset->{$jobset->get_column('project')}->{$jobset->name} += $costPerBuild;
blockBuilds($buildsByDrv, $blockedBuilds, $build);
next j;
}
} }
if ($started > 0) { last; # nothing found, give up on this system type
print STDERR "system type `", $system->system,
"': $nrActive active, $max allowed, started $started builds\n";
} }
} }
$lastTime = time();
$_->update({ starttime => time() }) foreach @buildsStarted;
}); });
# Actually start the builds we just selected. We need to do this # Actually start the builds we just selected. We need to do this
# outside the transaction in case it aborts or something. # outside the transaction in case it aborts or something.
foreach my $build (@buildsStarted) { foreach my $build (@buildsStarted) {
my $id = $build->id; my $id = $build->id;
print "starting build $id (", $build->project->name, ":", $build->jobset->name, ':', $build->job->name, ") on ", $build->system, "\n";
eval { eval {
my $logfile = $build->logfile; my $logfile = $build->logfile;
my $child = fork(); my $child = fork();
@ -164,9 +243,7 @@ sub checkBuilds {
if ($@) { if ($@) {
warn $@; warn $@;
txn_do($db, sub { txn_do($db, sub {
$build->busy(0); $build->update({ busy => 0, locker => $$ });
$build->locker($$);
$build->update;
}); });
} }
} }
@ -187,8 +264,6 @@ while (1) {
unlockDeadBuilds; unlockDeadBuilds;
checkBuilds; checkBuilds;
$lastTime = time();
}; };
warn $@ if $@; warn $@ if $@;

View file

@ -0,0 +1,58 @@
#! /var/run/current-system/sw/bin/perl -w
use strict;
use File::Basename;
use Fcntl;
use IO::File;
use Net::Amazon::S3;
use Net::Amazon::S3::Client;
use Nix::Config;
use Nix::Store;
use Hydra::Model::DB;
use Hydra::Helper::Nix;
my $cfg = getHydraConfig()->{s3backup};
my @config = defined $cfg ? ref $cfg eq "ARRAY" ? @$cfg : ($cfg) : ();
exit 0 unless @config;
my $lockfile = Hydra::Model::DB::getHydraPath . "/.hydra-s3backup.lock";
my $lockhandle = IO::File->new;
open($lockhandle, ">", $lockfile) or die "Opening $lockfile: $!";
flock($lockhandle, Fcntl::LOCK_EX) or die "Write-locking $lockfile: $!";
my $client = Net::Amazon::S3::Client->new( s3 => Net::Amazon::S3->new( retry => 1 ) );
my $db = Hydra::Model::DB->new();
my $gcRootsDir = getGCRootsDir;
opendir DIR, $gcRootsDir or die;
my @roots = readdir DIR;
closedir DIR;
my @actual_roots = ();
foreach my $link (@roots) {
next if $link eq "." || $link eq "..";
push @actual_roots, readlink "$gcRootsDir/$link";
}
# Don't delete a nix-cache-info file, if present
my %closure = ( "nix-cache-info" => undef );
foreach my $path (computeFSClosure(0, 0, @actual_roots)) {
my $hash = substr basename($path), 0, 32;
$closure{"$hash.narinfo"} = undef;
$closure{"$hash.nar"} = undef;
}
foreach my $bucket_config (@config) {
my $bucket = $client->bucket( name => $bucket_config->{name} );
my $prefix = exists $bucket_config->{prefix} ? $bucket_config->{prefix} : "";
my $cache_stream = $bucket->list({ prefix => $prefix });
until ($cache_stream->is_done) {
foreach my $object ($cache_stream->items) {
$object->delete unless exists $closure{basename($object->key)};
}
}
}
1;

View file

@ -56,6 +56,11 @@ my @roots = readdir DIR;
closedir DIR; closedir DIR;
# For scheduled builds, we register the derivation as a GC root.
print STDERR "*** looking for scheduled builds\n";
keepBuild $_ foreach $db->resultset('Builds')->search({ finished => 0 }, { columns => [ @columns ] });
# Keep every build in every release of every project. # Keep every build in every release of every project.
print STDERR "*** looking for release members\n"; print STDERR "*** looking for release members\n";
keepBuild $_ foreach $db->resultset('Builds')->search_literal( keepBuild $_ foreach $db->resultset('Builds')->search_literal(
@ -84,50 +89,33 @@ foreach my $project ($db->resultset('Projects')->search({}, { order_by => ["name
next; next;
} }
if ($keepnr <= 0 ) { print STDERR "*** looking for all builds in the unfinished and $keepnr most recent finished evaluations of jobset ",
print STDERR "*** jobset ", $project->name, ":", $jobset->name, " set to keep 0 builds\n";
next;
}
# FIXME: base this on jobset evals?
print STDERR "*** looking for the $keepnr most recent successful builds of each job in jobset ",
$project->name, ":", $jobset->name, "\n"; $project->name, ":", $jobset->name, "\n";
my @evals;
# Get the unfinished evals.
push @evals, $_->get_column("eval") foreach $jobset->builds->search(
{ finished => 0 },
{ join => "jobsetevalmembers", select => "jobsetevalmembers.eval", as => "eval", distinct => 1 });
# Get the N most recent finished evals.
if ($keepnr) {
push @evals, $_->get_column("id") foreach $jobset->jobsetevals->search(
{ hasNewBuilds => 1 },
{ where => \ "not exists (select 1 from builds b join jobsetevalmembers m on b.id = m.build where m.eval = me.id and b.finished = 0)"
, order_by => "id desc", rows => $keepnr });
}
keepBuild $_ foreach $jobset->builds->search( keepBuild $_ foreach $jobset->builds->search(
{ 'me.id' => { 'in' => \ { finished => 1, buildStatus => { -in => [0, 6] }
[ "select b2.id from Builds b2 join " . , id => { -in => $db->resultset('JobsetEvalMembers')->search({ eval => { -in => [@evals] } }, { select => "build" })->as_query }
" (select distinct job, system, coalesce( " .
" (select id from builds where project = b.project and jobset = b.jobset and job = b.job and system = b.system and finished = 1 and buildStatus = 0 order by id desc offset ? limit 1)" .
" , 0) as nth from builds b where project = ? and jobset = ? and isCurrent = 1) x " .
" on b2.project = ? and b2.jobset = ? and b2.job = x.job and b2.system = x.system and (id >= x.nth) where finished = 1 and buildStatus = 0"
, [ '', $keepnr - 1 ], [ '', $project->name ], [ '', $jobset->name ], [ '', $project->name ], [ '', $jobset->name ] ] }
}, },
{ order_by => ["job", "system", "id"], columns => [ @columns ] }); { order_by => ["job", "id"], columns => [ @columns ] });
}
# Go over all views in this project.
foreach my $view ($project->views->all) {
print STDERR "*** looking for builds to keep in view ", $project->name, ":", $view->name, "\n";
(my $primaryJob) = $view->viewjobs->search({isprimary => 1});
my $jobs = [$view->viewjobs->all];
# Keep all builds belonging to the most recent successful view result.
my $latest = getLatestSuccessfulViewResult($project, $primaryJob, $jobs, 0);
if (defined $latest) {
print STDERR " keeping latest successful view result ", $latest->id, " (", $latest->get_column('releasename'), ")\n";
my $result = getViewResult($latest, $jobs);
keepBuild $_->{build} foreach @{$result->{jobs}};
}
} }
} }
# For scheduled builds, we register the derivation as a GC root.
print STDERR "*** looking for scheduled builds\n";
keepBuild $_ foreach $db->resultset('Builds')->search({ finished => 0 }, { columns => [ @columns ] });
# Remove existing roots that are no longer wanted. # Remove existing roots that are no longer wanted.
print STDERR "*** removing unneeded GC roots\n"; print STDERR "*** removing unneeded GC roots\n";

View file

@ -55,12 +55,14 @@ create table Jobsets (
errorTime integer, -- timestamp associated with errorMsg errorTime integer, -- timestamp associated with errorMsg
lastCheckedTime integer, -- last time the evaluator looked at this jobset lastCheckedTime integer, -- last time the evaluator looked at this jobset
triggerTime integer, -- set if we were triggered by a push event triggerTime integer, -- set if we were triggered by a push event
enabled integer not null default 1, enabled integer not null default 1, -- 0 = disabled, 1 = enabled, 2 = one-shot
enableEmail integer not null default 1, enableEmail integer not null default 1,
hidden integer not null default 0, hidden integer not null default 0,
emailOverride text not null, emailOverride text not null,
keepnr integer not null default 3, keepnr integer not null default 3,
checkInterval integer not null default 300, -- minimum time in seconds between polls (0 = disable polling) checkInterval integer not null default 300, -- minimum time in seconds between polls (0 = disable polling)
schedulingShares integer not null default 100,
fetchErrorMsg text,
primary key (project, name), primary key (project, name),
foreign key (project) references Projects(name) on delete cascade on update cascade foreign key (project) references Projects(name) on delete cascade on update cascade
#ifdef SQLITE #ifdef SQLITE
@ -74,7 +76,8 @@ create table JobsetInputs (
project text not null, project text not null,
jobset text not null, jobset text not null,
name text not null, name text not null,
type text not null, -- "svn", "path", "uri", "string", "boolean" type text not null, -- "svn", "path", "uri", "string", "boolean", "nix"
emailResponsible integer not null default 0, -- whether to email committers to this input who change a build
primary key (project, jobset, name), primary key (project, jobset, name),
foreign key (project, jobset) references Jobsets(project, name) on delete cascade on update cascade foreign key (project, jobset) references Jobsets(project, name) on delete cascade on update cascade
); );
@ -140,7 +143,7 @@ create table Builds (
isCurrent integer default 0, isCurrent integer default 0,
-- Copy of the nixExprInput/nixExprPath fields of the jobset that -- Copy of the nixExprInput/nixExprPath fields of the jobset that
-- instantiated this build. Needed if we want to clone this -- instantiated this build. Needed if we want to reproduce this
-- build. -- build.
nixExprInput text, nixExprInput text,
nixExprPath text, nixExprPath text,
@ -255,6 +258,7 @@ create table BuildInputs (
uri text, uri text,
revision text, revision text,
value text, value text,
emailResponsible integer not null default 0,
dependency integer, -- build ID of the input, for type == 'build' dependency integer, -- build ID of the input, for type == 'build'
path text, path text,
@ -322,6 +326,15 @@ create table CachedGitInputs (
primary key (uri, branch, revision) primary key (uri, branch, revision)
); );
create table CachedDarcsInputs (
uri text not null,
revision text not null,
sha256hash text not null,
storePath text not null,
revCount integer not null,
primary key (uri, revision)
);
create table CachedHgInputs ( create table CachedHgInputs (
uri text not null, uri text not null,
branch text not null, branch text not null,
@ -514,6 +527,56 @@ create table NewsItems (
); );
create table AggregateConstituents (
aggregate integer not null references Builds(id) on delete cascade,
constituent integer not null references Builds(id) on delete cascade,
primary key (aggregate, constituent)
);
create table StarredJobs (
userName text not null,
project text not null,
jobset text not null,
job text not null,
primary key (userName, project, jobset, job),
foreign key (userName) references Users(userName) on update cascade on delete cascade,
foreign key (project) references Projects(name) on update cascade on delete cascade,
foreign key (project, jobset) references Jobsets(project, name) on update cascade on delete cascade,
foreign key (project, jobset, job) references Jobs(project, jobset, name) on update cascade on delete cascade
);
-- Cache of the number of finished builds.
create table NrBuilds (
what text primary key not null,
count integer not null
);
insert into NrBuilds(what, count) values('finished', 0);
#ifdef POSTGRESQL
create function modifyNrBuildsFinished() returns trigger as $$
begin
if ((tg_op = 'INSERT' and new.finished = 1) or
(tg_op = 'UPDATE' and old.finished = 0 and new.finished = 1)) then
update NrBuilds set count = count + 1 where what = 'finished';
elsif ((tg_op = 'DELETE' and old.finished = 1) or
(tg_op = 'UPDATE' and old.finished = 1 and new.finished = 0)) then
update NrBuilds set count = count - 1 where what = 'finished';
end if;
return null;
end;
$$ language plpgsql;
create trigger NrBuildsFinished after insert or update or delete on Builds
for each row
execute procedure modifyNrBuildsFinished();
#endif
-- Some indices. -- Some indices.
create index IndexBuildInputsOnBuild on BuildInputs(build); create index IndexBuildInputsOnBuild on BuildInputs(build);
@ -534,7 +597,8 @@ create index IndexBuildsOnJobAndSystem on Builds(project, jobset, job, system);
create index IndexBuildsOnJobset on Builds(project, jobset); create index IndexBuildsOnJobset on Builds(project, jobset);
create index IndexBuildsOnProject on Builds(project); create index IndexBuildsOnProject on Builds(project);
create index IndexBuildsOnTimestamp on Builds(timestamp); create index IndexBuildsOnTimestamp on Builds(timestamp);
create index IndexBuildsOnJobsetFinishedTimestamp on Builds(project, jobset, finished, timestamp DESC); create index IndexBuildsOnFinishedStopTime on Builds(finished, stoptime DESC);
create index IndexBuildsOnJobsetFinishedTimestamp on Builds(project, jobset, finished, timestamp DESC); -- obsolete?
create index IndexBuildsOnJobFinishedId on builds(project, jobset, job, system, finished, id DESC); create index IndexBuildsOnJobFinishedId on builds(project, jobset, job, system, finished, id DESC);
create index IndexBuildsOnJobSystemCurrent on Builds(project, jobset, job, system, isCurrent); create index IndexBuildsOnJobSystemCurrent on Builds(project, jobset, job, system, isCurrent);
create index IndexBuildsOnDrvPath on Builds(drvPath); create index IndexBuildsOnDrvPath on Builds(drvPath);

23
src/sql/upgrade-17.sql Normal file
View file

@ -0,0 +1,23 @@
create table NrBuilds (
what text primary key not null,
count integer not null
);
create function modifyNrBuildsFinished() returns trigger as $$
begin
if ((tg_op = 'INSERT' and new.finished = 1) or
(tg_op = 'UPDATE' and old.finished = 0 and new.finished = 1)) then
update NrBuilds set count = count + 1 where what = 'finished';
elsif ((tg_op = 'DELETE' and old.finished = 1) or
(tg_op = 'UPDATE' and old.finished = 1 and new.finished = 0)) then
update NrBuilds set count = count - 1 where what = 'finished';
end if;
return null;
end;
$$ language plpgsql;
create trigger NrBuildsFinished after insert or update or delete on Builds
for each row
execute procedure modifyNrBuildsFinished();
insert into NrBuilds(what, count) select 'finished', count(*) from Builds where finished = 1;

1
src/sql/upgrade-18.sql Normal file
View file

@ -0,0 +1 @@
create index IndexBuildsOnFinishedStopTime on Builds(finished, stoptime DESC);

5
src/sql/upgrade-19.sql Normal file
View file

@ -0,0 +1,5 @@
create table AggregateConstituents (
aggregate integer not null references Builds(id) on delete cascade,
constituent integer not null references Builds(id) on delete cascade,
primary key (aggregate, constituent)
);

8
src/sql/upgrade-20.sql Normal file
View file

@ -0,0 +1,8 @@
create table CachedDarcsInputs (
uri text not null,
revision text not null,
sha256hash text not null,
storePath text not null,
revCount integer not null,
primary key (uri, revision)
);

Some files were not shown because too many files have changed in this diff Show more