forked from lix-project/hydra
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:
commit
b54cfbf032
|
@ -1,4 +1,8 @@
|
|||
SUBDIRS = src tests doc
|
||||
BOOTCLEAN_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/
|
||||
|
|
|
@ -52,8 +52,7 @@ then
|
|||
NIX_STATE_DIR="$TMPDIR"
|
||||
export NIX_STATE_DIR
|
||||
fi
|
||||
if "$NIX_STORE_PROGRAM" --timeout 123 -q > /dev/null 2>&1
|
||||
then
|
||||
if NIX_REMOTE=daemon "$NIX_STORE_PROGRAM" --timeout 123 -q; then
|
||||
AC_MSG_RESULT([yes])
|
||||
else
|
||||
AC_MSG_RESULT([no])
|
||||
|
@ -68,7 +67,7 @@ LDFLAGS="$LDFLAGS -L$nix/lib/nix"
|
|||
|
||||
AC_CHECK_HEADER([store-api.hh], [:],
|
||||
[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.])])
|
||||
|
||||
CPPFLAGS="$old_CPPFLAGS"
|
||||
|
|
7
dev-shell
Executable file
7
dev-shell
Executable 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" "$@"
|
|
@ -127,3 +127,13 @@
|
|||
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;
|
||||
|
||||
|
||||
* 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;
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
39
doc/manual/hacking.xml
Normal file
39
doc/manual/hacking.xml
Normal 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>
|
|
@ -52,8 +52,7 @@
|
|||
|
||||
|
||||
<copyright>
|
||||
<year>2009</year>
|
||||
<year>2010</year>
|
||||
<year>2009-2013</year>
|
||||
<holder>Eelco Dolstra</holder>
|
||||
</copyright>
|
||||
|
||||
|
@ -64,6 +63,7 @@
|
|||
<xi:include href="introduction.xml" />
|
||||
<xi:include href="installation.xml" />
|
||||
<xi:include href="projects.xml" />
|
||||
<xi:include href="hacking.xml" />
|
||||
|
||||
|
||||
</book>
|
||||
|
|
|
@ -7,13 +7,7 @@ let
|
|||
|
||||
baseDir = "/var/lib/hydra";
|
||||
|
||||
hydraConf = pkgs.writeScript "hydra.conf"
|
||||
''
|
||||
using_frontend_proxy 1
|
||||
base_uri ${cfg.hydraURL}
|
||||
notification_sender ${cfg.notificationSender}
|
||||
max_servers 25
|
||||
'';
|
||||
hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig;
|
||||
|
||||
env =
|
||||
{ NIX_REMOTE = "daemon";
|
||||
|
@ -28,7 +22,7 @@ let
|
|||
serverEnv = env //
|
||||
{ HYDRA_LOGO = if cfg.logo != null then cfg.logo else "";
|
||||
HYDRA_TRACKER = cfg.tracker;
|
||||
};
|
||||
} // (optionalAttrs cfg.debugServer { DBIC_TRACE = 1; });
|
||||
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 {
|
||||
default = 3000;
|
||||
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
|
||||
|
||||
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 ];
|
||||
|
||||
users.extraUsers.hydra =
|
||||
|
@ -151,14 +173,36 @@ in
|
|||
|
||||
systemd.services."hydra-init" =
|
||||
{ wantedBy = [ "multi-user.target" ];
|
||||
requires = [ "postgresql.service" ];
|
||||
after = [ "postgresql.service" ];
|
||||
environment = env;
|
||||
script = ''
|
||||
mkdir -p ${baseDir}/data
|
||||
chown hydra ${baseDir}/data
|
||||
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
|
||||
${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.RemainAfterExit = true;
|
||||
};
|
||||
|
||||
systemd.services."hydra-server" =
|
||||
|
@ -167,7 +211,7 @@ in
|
|||
after = [ "hydra-init.service" ];
|
||||
environment = serverEnv;
|
||||
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";
|
||||
Restart = "always";
|
||||
};
|
||||
|
@ -177,7 +221,7 @@ in
|
|||
{ wantedBy = [ "multi-user.target" ];
|
||||
wants = [ "hydra-init.service" ];
|
||||
after = [ "hydra-init.service" "network.target" ];
|
||||
path = [ pkgs.nettools pkgs.ssmtp ];
|
||||
path = [ pkgs.nettools ];
|
||||
environment = env;
|
||||
serviceConfig =
|
||||
{ ExecStartPre = "${cfg.hydra}/bin/hydra-queue-runner --unlock";
|
||||
|
@ -191,7 +235,7 @@ in
|
|||
{ wantedBy = [ "multi-user.target" ];
|
||||
wants = [ "hydra-init.service" ];
|
||||
after = [ "hydra-init.service" "network.target" ];
|
||||
path = [ pkgs.nettools pkgs.ssmtp ];
|
||||
path = [ pkgs.nettools ];
|
||||
environment = env;
|
||||
serviceConfig =
|
||||
{ ExecStart = "@${cfg.hydra}/bin/hydra-evaluator hydra-evaluator";
|
||||
|
@ -227,8 +271,9 @@ in
|
|||
|
||||
compressLogs = pkgs.writeScript "compress-logs" ''
|
||||
#! ${pkgs.stdenv.shell} -e
|
||||
touch -d 'last month' r
|
||||
find /nix/var/log/nix/drvs -type f -a ! -newer r -name '*.drv' | xargs bzip2 -v
|
||||
find /nix/var/log/nix/drvs \
|
||||
-type f -a ! -newermt 'last month' \
|
||||
-name '*.drv' -exec bzip2 -v {} +
|
||||
'';
|
||||
in
|
||||
[ "*/5 * * * * root ${checkSpace} &> ${baseDir}/data/checkspace.log"
|
||||
|
|
70
release.nix
70
release.nix
|
@ -6,7 +6,7 @@ let
|
|||
|
||||
pkgs = import <nixpkgs> {};
|
||||
|
||||
genAttrs' = pkgs.lib.genAttrs [ "x86_64-linux" "i686-linux" ];
|
||||
genAttrs' = pkgs.lib.genAttrs [ "x86_64-linux" ];
|
||||
|
||||
in rec {
|
||||
|
||||
|
@ -24,13 +24,17 @@ in rec {
|
|||
|
||||
versionSuffix = if officialRelease then "" else "pre${toString hydraSrc.revCount}-${hydraSrc.gitTag}";
|
||||
|
||||
preConfigure = ''
|
||||
preHook = ''
|
||||
# TeX needs a writable font cache.
|
||||
export VARTEXFONTS=$TMPDIR/texfonts
|
||||
|
||||
addToSearchPath PATH $(pwd)/src/script
|
||||
addToSearchPath PATH $(pwd)/src/c
|
||||
addToSearchPath PERL5LIB $(pwd)/src/lib
|
||||
'';
|
||||
|
||||
configureFlags =
|
||||
[ "--with-nix=${nix}"
|
||||
[ "--with-nix=${nixUnstable}"
|
||||
"--with-docbook-xsl=${docbook_xsl}/xml/xsl/docbook"
|
||||
];
|
||||
|
||||
|
@ -88,6 +92,7 @@ in rec {
|
|||
PadWalker
|
||||
CatalystDevel
|
||||
Readonly
|
||||
SetScalar
|
||||
SQLSplitStatement
|
||||
Starman
|
||||
SysHostnameLong
|
||||
|
@ -95,6 +100,7 @@ in rec {
|
|||
TextDiff
|
||||
TextTable
|
||||
XMLSimple
|
||||
NetAmazonS3
|
||||
nix git
|
||||
];
|
||||
};
|
||||
|
@ -108,17 +114,20 @@ in rec {
|
|||
|
||||
buildInputs =
|
||||
[ 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
|
||||
perlDeps perl
|
||||
];
|
||||
|
||||
hydraPath = lib.makeSearchPath "bin" (
|
||||
[ 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 ] );
|
||||
|
||||
preConfigure = "patchShebangs .";
|
||||
preCheck = ''
|
||||
patchShebangs .
|
||||
export LOGNAME=${LOGNAME:-foo}
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
mkdir -p $out/nix-support
|
||||
|
@ -134,14 +143,13 @@ in rec {
|
|||
done
|
||||
''; # */
|
||||
|
||||
LOGNAME = "foo";
|
||||
|
||||
meta.description = "Build of Hydra on ${system}";
|
||||
passthru.perlDeps = perlDeps;
|
||||
});
|
||||
|
||||
|
||||
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}
|
||||
simpleTest {
|
||||
machine =
|
||||
|
@ -169,8 +177,8 @@ in rec {
|
|||
});
|
||||
|
||||
tests.api = genAttrs' (system:
|
||||
with import <nixos/lib/testing.nix> { inherit system; };
|
||||
let hydra = builtins.getAttr system build; in # build.${system}
|
||||
with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
|
||||
let hydra = builtins.getAttr system build; in # build."${system}"
|
||||
simpleTest {
|
||||
machine =
|
||||
{ config, pkgs, ... }:
|
||||
|
@ -178,6 +186,7 @@ in rec {
|
|||
services.postgresql.package = pkgs.postgresql92;
|
||||
environment.systemPackages = [ hydra pkgs.perlPackages.LWP pkgs.perlPackages.JSON ];
|
||||
virtualisation.memorySize = 2047;
|
||||
boot.kernelPackages = pkgs.linuxPackages_3_10;
|
||||
};
|
||||
|
||||
testScript =
|
||||
|
@ -204,4 +213,43 @@ in rec {
|
|||
$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");
|
||||
'';
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
const ArgsUsed & argsUsed, const AutoArgs & argsLeft,
|
||||
Value & v, const string & attrPath)
|
||||
|
@ -136,8 +152,9 @@ static void findJobsWrapped(EvalState & state, XMLWriter & doc,
|
|||
MetaInfo meta = drv.queryMetaInfo(state);
|
||||
xmlAttrs["description"] = queryMetaFieldString(meta, "description");
|
||||
xmlAttrs["longDescription"] = queryMetaFieldString(meta, "longDescription");
|
||||
xmlAttrs["license"] = queryMetaFieldString(meta, "license");
|
||||
xmlAttrs["license"] = queryMetaField(meta, "license");
|
||||
xmlAttrs["homepage"] = queryMetaFieldString(meta, "homepage");
|
||||
xmlAttrs["maintainers"] = queryMetaField(meta, "maintainers");
|
||||
|
||||
int prio = queryMetaFieldInt(meta, "schedulingPriority", 100);
|
||||
xmlAttrs["schedulingPriority"] = int2String(prio);
|
||||
|
@ -148,17 +165,22 @@ static void findJobsWrapped(EvalState & state, XMLWriter & doc,
|
|||
int maxsilent = queryMetaFieldInt(meta, "maxSilent", 3600);
|
||||
xmlAttrs["maxSilent"] = int2String(maxsilent);
|
||||
|
||||
string maintainers;
|
||||
MetaValue value = meta["maintainers"];
|
||||
if (value.type == MetaValue::tpString)
|
||||
maintainers = value.stringValue;
|
||||
else if (value.type == MetaValue::tpStrings) {
|
||||
foreach (Strings::const_iterator, i, value.stringValues) {
|
||||
if (maintainers.size() != 0) maintainers += ", ";
|
||||
maintainers += *i;
|
||||
/* If this is an aggregate, then get its constituents. */
|
||||
Bindings::iterator a = v.attrs->find(state.symbols.create("_hydraAggregate"));
|
||||
if (a != v.attrs->end() && state.forceBool(*a->value)) {
|
||||
Bindings::iterator a = v.attrs->find(state.symbols.create("constituents"));
|
||||
if (a == v.attrs->end())
|
||||
throw EvalError("derivation must have a ‘constituents’ attribute");
|
||||
PathSet context;
|
||||
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
|
||||
registers roots for jobs that we may have already
|
||||
|
@ -267,9 +289,8 @@ void run(Strings args)
|
|||
|
||||
store = openStore();
|
||||
|
||||
Expr * e = state.parseExprFromFile(releaseExpr);
|
||||
Value v;
|
||||
state.mkThunk_(v, e);
|
||||
state.evalFile(releaseExpr, v);
|
||||
|
||||
XMLWriter doc(true, std::cout);
|
||||
XMLOpenElement root(doc, "jobs");
|
||||
|
|
|
@ -7,46 +7,6 @@ use Hydra::Helper::Nix;
|
|||
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 {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
|
@ -56,13 +16,12 @@ sub all : Chained('get_builds') PathPart {
|
|||
|
||||
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->{page} = $page;
|
||||
$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(
|
||||
{ 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({}, { columns => [@buildListColumns, 'drvpath', 'description', 'homepage']
|
||||
, join => ["buildoutputs"]
|
||||
, order_by => ["me.id", "buildoutputs.name"]
|
||||
, '+select' => ['buildoutputs.path', 'buildoutputs.name'], '+as' => ['outpath', 'outname'] });
|
||||
}
|
||||
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;
|
||||
|
|
|
@ -14,20 +14,36 @@ sub getChannelData {
|
|||
|
||||
my @storePaths = ();
|
||||
$c->stash->{nixPkgs} = [];
|
||||
foreach my $build ($c->stash->{channelBuilds}->all) {
|
||||
my $outPath = $build->get_column("outpath");
|
||||
my $outName = $build->get_column("outname");
|
||||
|
||||
my @builds = $c->stash->{channelBuilds}->all;
|
||||
|
||||
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);
|
||||
$outputs->{$output->get_column("outname")} = $outPath;
|
||||
push @storePaths, $outPath;
|
||||
my $pkgName = $build->nixname . "-" . $build->system . "-" . $build->id . ($outName ne "out" ? "-" . $outName : "");
|
||||
push @{$c->stash->{nixPkgs}}, { build => $build, name => $pkgName, outPath => $outPath, outName => $outName };
|
||||
# Put the system type in the manifest (for top-level paths) as
|
||||
# a hint to the binary patch generator. (It shouldn't try to
|
||||
# generate patches between builds for different systems.) It
|
||||
# would be nice if Nix stored this info for every path but it
|
||||
# doesn't.
|
||||
# Put the system type in the manifest (for top-level
|
||||
# paths) as a hint to the binary patch generator. (It
|
||||
# shouldn't try to generate patches between builds for
|
||||
# different systems.) It would be nice if Nix stored this
|
||||
# info for every path but it doesn't.
|
||||
$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];
|
||||
}
|
||||
|
|
|
@ -4,8 +4,12 @@ use strict;
|
|||
use warnings;
|
||||
use base 'Catalyst::Controller::REST';
|
||||
|
||||
# Hack: Erase the map set by C::C::REST
|
||||
__PACKAGE__->config( map => undef );
|
||||
__PACKAGE__->config(
|
||||
map => {
|
||||
'application/json' => 'JSON',
|
||||
'text/x-json' => 'JSON',
|
||||
'text/html' => [ 'View', 'TT' ]
|
||||
},
|
||||
default => 'text/html',
|
||||
|
|
|
@ -8,36 +8,26 @@ use base 'DBIx::Class';
|
|||
|
||||
sub TO_JSON {
|
||||
my $self = shift;
|
||||
my $json = { $self->get_columns };
|
||||
my $rs = $self->result_source;
|
||||
my @relnames = $rs->relationships;
|
||||
RELLOOP: foreach my $relname (@relnames) {
|
||||
my $relinfo = $rs->relationship_info($relname);
|
||||
next unless defined $relinfo->{attrs}->{accessor};
|
||||
my $accessor = $relinfo->{attrs}->{accessor};
|
||||
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);
|
||||
|
||||
my $hint = $self->json_hint;
|
||||
|
||||
my %json = ();
|
||||
|
||||
foreach my $column (@{$hint->{columns}}) {
|
||||
$json{$column} = $self->get_column($column);
|
||||
}
|
||||
} #!!! 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") {
|
||||
$json->{$relname} = [ map { $_->TO_JSON } $self->$relname ];
|
||||
} else {
|
||||
$json->{$relname} = $self->$relname->TO_JSON;
|
||||
|
||||
foreach my $relname (keys %{$hint->{eager_relations}}) {
|
||||
my $key = $hint->{eager_relations}->{$relname};
|
||||
$json{$relname} = { map { $_->$key => $_ } $self->$relname };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $json;
|
||||
|
||||
return \%json;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -15,8 +15,6 @@ use Digest::SHA qw(sha256_hex);
|
|||
use Text::Diff;
|
||||
use File::Slurp;
|
||||
|
||||
# !!! Rewrite this to use View::JSON.
|
||||
|
||||
|
||||
sub api : Chained('/') PathPart('api') CaptureArgs(0) {
|
||||
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 {
|
||||
my ($build) = @_;
|
||||
my $result = {
|
||||
|
|
|
@ -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) {
|
||||
my ($self, $c) = @_;
|
||||
my $time = time();
|
||||
$c->model('DB::Builds')->search({finished => 0, iscurrent => 0, busy => 0})->update({ finished => 1, buildstatus => 4, starttime => $time, stoptime => $time });
|
||||
my $builds = $c->model('DB::Builds')->search(
|
||||
{ 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");
|
||||
}
|
||||
|
||||
|
@ -49,19 +53,11 @@ sub clearfailedcache : Chained('admin') PathPart('clear-failed-cache') Args(0) {
|
|||
|
||||
sub clearvcscache : Chained('admin') PathPart('clear-vcs-cache') Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
print STDERR "Clearing path cache\n";
|
||||
$c->model('DB::CachedPathInputs')->delete_all;
|
||||
|
||||
print STDERR "Clearing git cache\n";
|
||||
$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->model('DB::CachedPathInputs')->delete;
|
||||
$c->model('DB::CachedGitInputs')->delete;
|
||||
$c->model('DB::CachedSubversionInputs')->delete;
|
||||
$c->model('DB::CachedBazaarInputs')->delete;
|
||||
$c->flash->{successMsg} = "VCS caches have been cleared.";
|
||||
$c->res->redirect($c->request->referer // "/admin");
|
||||
}
|
||||
|
||||
|
|
|
@ -35,18 +35,18 @@ sub buildChain :Chained('/') :PathPart('build') :CaptureArgs(1) {
|
|||
|
||||
|
||||
sub findBuildStepByOutPath {
|
||||
my ($self, $c, $path, $status) = @_;
|
||||
my ($self, $c, $path) = @_;
|
||||
return $c->model('DB::BuildSteps')->search(
|
||||
{ path => $path, busy => 0, status => $status },
|
||||
{ join => ["buildstepoutputs"], order_by => ["stopTime"], limit => 1 })->single;
|
||||
{ path => $path, busy => 0 },
|
||||
{ join => ["buildstepoutputs"], order_by => ["status", "stopTime"], rows => 1 })->single;
|
||||
}
|
||||
|
||||
|
||||
sub findBuildStepByDrvPath {
|
||||
my ($self, $c, $drvPath, $status) = @_;
|
||||
my ($self, $c, $drvPath) = @_;
|
||||
return $c->model('DB::BuildSteps')->search(
|
||||
{ drvpath => $drvPath, busy => 0, status => $status },
|
||||
{ order_by => ["stopTime"], limit => 1 })->single;
|
||||
{ drvpath => $drvPath, busy => 0 },
|
||||
{ order_by => ["status", "stopTime"], rows => 1 })->single;
|
||||
}
|
||||
|
||||
|
||||
|
@ -60,7 +60,6 @@ sub build_GET {
|
|||
$c->stash->{template} = 'build.tt';
|
||||
$c->stash->{available} = all { isValidPath($_->path) } $build->buildoutputs->all;
|
||||
$c->stash->{drvAvailable} = isValidPath $build->drvpath;
|
||||
$c->stash->{flashMsg} = $c->flash->{buildMsg};
|
||||
|
||||
if (!$build->finished && $build->busy) {
|
||||
$c->stash->{logtext} = read_file($build->logfile, err_mode => 'quiet') // "";
|
||||
|
@ -68,8 +67,7 @@ sub build_GET {
|
|||
|
||||
if ($build->finished && $build->iscachedbuild) {
|
||||
my $path = ($build->buildoutputs)[0]->path or die;
|
||||
my $cachedBuildStep = findBuildStepByOutPath($self, $c, $path,
|
||||
$build->buildstatus == 0 || $build->buildstatus == 6 ? 0 : 1);
|
||||
my $cachedBuildStep = findBuildStepByOutPath($self, $c, $path);
|
||||
$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.
|
||||
($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},
|
||||
{ limit => 1, order_by => ["id"] });
|
||||
{ rows => 1, order_by => ["id"] })->single;
|
||||
$self->status_ok(
|
||||
$c,
|
||||
entity => $c->model('DB::Builds')->find($build->id,{
|
||||
columns => [
|
||||
'id',
|
||||
'finished',
|
||||
'timestamp',
|
||||
'buildstatus',
|
||||
'job',
|
||||
'project',
|
||||
'jobset',
|
||||
'starttime',
|
||||
'stoptime',
|
||||
]
|
||||
})
|
||||
entity => $build
|
||||
);
|
||||
|
||||
# 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;
|
||||
|
||||
showLog($c, $step->drvpath, $mode);
|
||||
showLog($c, $mode, $step->drvpath, map { $_->path } $step->buildstepoutputs->all);
|
||||
}
|
||||
|
||||
|
||||
sub view_log : Chained('buildChain') PathPart('log') {
|
||||
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 {
|
||||
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;
|
||||
|
||||
my $size = stat($logPath)->size;
|
||||
error($c, "This build log is too big to display ($size bytes).")
|
||||
if $size >= 64 * 1024 * 1024;
|
||||
|
||||
if (!$mode) {
|
||||
# !!! 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") . " -"
|
||||
. " | xsltproc " . $c->path_to("xsl/log2html.xsl") . " - | tail -n +2";
|
||||
$c->stash->{template} = 'log.tt';
|
||||
$c->stash->{logtext} = `$pipeline`;
|
||||
$c->stash->{logtext} = `ulimit -t 5 ; $pipeline`;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
elsif ($mode eq "tail-reload") {
|
||||
my $url = $c->request->uri->as_string;
|
||||
|
@ -162,12 +159,12 @@ sub showLog {
|
|||
$c->stash->{url} = $url;
|
||||
$c->stash->{reload} = !$c->stash->{build}->finished && $c->stash->{build}->busy;
|
||||
$c->stash->{title} = "";
|
||||
$c->stash->{contents} = (scalar logContents($drvPath, 50)) || " ";
|
||||
$c->stash->{contents} = (scalar logContents($logPath, 50)) || " ";
|
||||
$c->stash->{template} = 'plain-reload.tt';
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
# link to some build product of the latest build (i.e. in conjunction
|
||||
# with the .../latest redirect).
|
||||
|
@ -269,7 +281,7 @@ sub contents : Chained('buildChain') PathPart Args(1) {
|
|||
notFound($c, "Product $path has disappeared.") unless -e $path;
|
||||
|
||||
# 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.
|
||||
|
||||
|
@ -339,8 +351,8 @@ sub getDependencyGraph {
|
|||
{ path => $path
|
||||
, name => $name
|
||||
, buildStep => $runtime
|
||||
? findBuildStepByOutPath($self, $c, $path, 0)
|
||||
: findBuildStepByDrvPath($self, $c, $path, 0)
|
||||
? findBuildStepByOutPath($self, $c, $path)
|
||||
: findBuildStepByDrvPath($self, $c, $path)
|
||||
};
|
||||
$$done{$path} = $node;
|
||||
my @refs;
|
||||
|
@ -409,49 +421,22 @@ sub nix : Chained('buildChain') PathPart('nix') CaptureArgs(0) {
|
|||
|
||||
sub restart : Chained('buildChain') PathPart Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
my $build = $c->stash->{build};
|
||||
|
||||
requireProjectOwner($c, $build->project);
|
||||
|
||||
my $drvpath = $build->drvpath;
|
||||
error($c, "This build cannot be restarted.")
|
||||
unless $build->finished && -f $drvpath;
|
||||
|
||||
restartBuild($c->model('DB')->schema, $build);
|
||||
|
||||
$c->flash->{buildMsg} = "Build has been restarted.";
|
||||
|
||||
my $n = restartBuilds($c->model('DB')->schema, $c->model('DB::Builds')->search({ id => $build->id }));
|
||||
error($c, "This build cannot be restarted.") if $n != 1;
|
||||
$c->flash->{successMsg} = "Build has been restarted.";
|
||||
$c->res->redirect($c->uri_for($self->action_for("build"), $c->req->captures));
|
||||
}
|
||||
|
||||
|
||||
sub cancel : Chained('buildChain') PathPart Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
my $build = $c->stash->{build};
|
||||
|
||||
requireProjectOwner($c, $build->project);
|
||||
|
||||
txn_do($c->model('DB')->schema, sub {
|
||||
error($c, "This build cannot be 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.";
|
||||
|
||||
my $n = cancelBuilds($c->model('DB')->schema, $c->model('DB::Builds')->search({ id => $build->id }));
|
||||
error($c, "This build cannot be cancelled.") if $n != 1;
|
||||
$c->flash->{successMsg} = "Build has been cancelled.";
|
||||
$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});
|
||||
});
|
||||
|
||||
$c->flash->{buildMsg} =
|
||||
$c->flash->{successMsg} =
|
||||
$keep ? "Build will be kept." : "Build will not be kept.";
|
||||
|
||||
$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});
|
||||
|
||||
$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));
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
my ($self, $c) = @_;
|
||||
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) {
|
||||
my ($self, $c) = @_;
|
||||
$c->response->content_type('text/x-shellscript');
|
||||
|
|
|
@ -20,24 +20,52 @@ sub job : Chained('/') PathPart('job') CaptureArgs(3) {
|
|||
|
||||
sub overview : Chained('job') PathPart('') Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
my $job = $c->stash->{job};
|
||||
|
||||
$c->stash->{template} = 'job.tt';
|
||||
|
||||
$c->stash->{lastBuilds} =
|
||||
[ $c->stash->{job}->builds->search({ finished => 1 },
|
||||
[ $job->builds->search({ finished => 1 },
|
||||
{ order_by => 'id DESC', rows => 10, columns => [@buildListColumns] }) ];
|
||||
|
||||
$c->stash->{queuedBuilds} = [
|
||||
$c->stash->{job}->builds->search(
|
||||
$job->builds->search(
|
||||
{ finished => 0 },
|
||||
{ join => ['project']
|
||||
, order_by => ["priority DESC", "id"]
|
||||
, '+select' => ['project.enabled']
|
||||
, '+as' => ['enabled']
|
||||
}
|
||||
{ order_by => ["priority DESC", "id"] }
|
||||
) ];
|
||||
|
||||
$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) {
|
||||
my ($self, $c) = @_;
|
||||
$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')
|
||||
->search({}, {bind => [$c->stash->{project}->name, $c->stash->{jobset}->name, $c->stash->{job}->name]});
|
||||
$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,5 +1,6 @@
|
|||
package Hydra::Controller::Jobset;
|
||||
|
||||
use utf8;
|
||||
use strict;
|
||||
use warnings;
|
||||
use base 'Hydra::Base::Controller::ListBuilds';
|
||||
|
@ -9,35 +10,18 @@ use Hydra::Helper::CatalystUtils;
|
|||
|
||||
sub jobsetChain :Chained('/') :PathPart('jobset') :CaptureArgs(2) {
|
||||
my ($self, $c, $projectName, $jobsetName) = @_;
|
||||
$c->stash->{params}->{name} //= $jobsetName;
|
||||
|
||||
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->{jobset_} = $project->jobsets->search({'me.name' => $jobsetName});
|
||||
my $jobset = $c->stash->{jobset_}->single;
|
||||
$c->stash->{jobset} = $project->jobsets->find({ name => $jobsetName });
|
||||
|
||||
if ($jobset) {
|
||||
$c->stash->{jobset} = $jobset;
|
||||
} 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;
|
||||
}
|
||||
notFound($c, "Jobset ‘$jobsetName’ doesn't exist.")
|
||||
if !$c->stash->{jobset} && !($c->action->name eq "jobset" and $c->request->method eq "PUT");
|
||||
}
|
||||
|
||||
|
||||
|
@ -50,26 +34,11 @@ sub jobset_GET {
|
|||
|
||||
$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,
|
||||
entity => $c->stash->{jobset_}->find({}, {
|
||||
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"
|
||||
})
|
||||
);
|
||||
$c->stash->{totalShares} = getTotalShares($c->model('DB')->schema);
|
||||
|
||||
$self->status_ok($c, entity => $c->stash->{jobset});
|
||||
}
|
||||
|
||||
sub jobset_PUT {
|
||||
|
@ -78,133 +47,91 @@ sub jobset_PUT {
|
|||
requireProjectOwner($c, $c->stash->{project});
|
||||
|
||||
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 {
|
||||
updateJobset($c, $c->stash->{jobset});
|
||||
});
|
||||
|
||||
if ($c->req->looks_like_browser) {
|
||||
$c->res->redirect($c->uri_for($self->action_for("jobset"),
|
||||
[$c->stash->{project}->name, $c->stash->{jobset}->name]) . "#tabs-configuration");
|
||||
} else {
|
||||
$self->status_no_content($c);
|
||||
my $uri = $c->uri_for($self->action_for("jobset"), [$c->stash->{project}->name, $c->stash->{jobset}->name]) . "#tabs-configuration";
|
||||
$self->status_ok($c, entity => { redirect => "$uri" });
|
||||
|
||||
$c->flash->{successMsg} = "The jobset configuration has been updated.";
|
||||
}
|
||||
} 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;
|
||||
txn_do($c->model('DB')->schema, sub {
|
||||
# Note: $jobsetName is validated in updateProject, which will
|
||||
# abort the transaction if the name isn't valid.
|
||||
$jobset = $c->stash->{project}->jobsets->create(
|
||||
{name => $c->stash->{jobsetName}, nixexprinput => "", nixexprpath => "", emailoverride => ""});
|
||||
{name => ".tmp", nixexprinput => "", nixexprpath => "", emailoverride => ""});
|
||||
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,
|
||||
$self->status_created($c,
|
||||
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) {
|
||||
my ($self, $c) = @_;
|
||||
$c->stash->{template} = 'jobset-jobs-tab.tt';
|
||||
|
||||
$c->stash->{activeJobs} = [];
|
||||
$c->stash->{inactiveJobs} = [];
|
||||
$c->stash->{filter} = $c->request->params->{filter} // "";
|
||||
my $filter = "%" . $c->stash->{filter} . "%";
|
||||
|
||||
(my $latestEval) = $c->stash->{jobset}->jobsetevals->search(
|
||||
{ hasnewbuilds => 1}, { limit => 1, order_by => ["id desc"] });
|
||||
my @evals = $c->stash->{jobset}->jobsetevals->search({ hasnewbuilds => 1}, { order_by => "id desc", rows => 20 });
|
||||
|
||||
my %activeJobs;
|
||||
if (defined $latestEval) {
|
||||
foreach my $build ($latestEval->builds->search({}, { order_by => ["job"], select => ["job"] })) {
|
||||
my $job = $build->get_column("job");
|
||||
if (!defined $activeJobs{$job}) {
|
||||
$activeJobs{$job} = 1;
|
||||
push @{$c->stash->{activeJobs}}, $job;
|
||||
}
|
||||
my $evals = {};
|
||||
my %jobs;
|
||||
my $nrBuilds = 0;
|
||||
|
||||
foreach my $eval (@evals) {
|
||||
my @builds = $eval->builds->search(
|
||||
{ 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 (!defined $activeJobs{$job->name}) {
|
||||
push @{$c->stash->{inactiveJobs}}, $job->name;
|
||||
if ($c->request->params->{showInactive}) {
|
||||
$c->stash->{showInactive} = 1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub status_tab : Chained('jobsetChain') PathPart('status-tab') Args(0) {
|
||||
my ($self, $c) = @_;
|
||||
$c->stash->{template} = 'jobset-status-tab.tt';
|
||||
|
||||
# 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"]
|
||||
}) ];
|
||||
$c->stash->{evals} = $evals;
|
||||
my @jobs = sort (keys %jobs);
|
||||
$c->stash->{nrJobs} = scalar @jobs;
|
||||
splice @jobs, 250 if $c->stash->{filter} eq "";
|
||||
$c->stash->{jobs} = [@jobs];
|
||||
}
|
||||
|
||||
|
||||
|
@ -212,10 +139,6 @@ sub status_tab : Chained('jobsetChain') PathPart('status-tab') Args(0) {
|
|||
sub get_builds : Chained('jobsetChain') PathPart('') CaptureArgs(0) {
|
||||
my ($self, $c) = @_;
|
||||
$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')
|
||||
->search({}, {bind => [$c->stash->{project}->name, $c->stash->{jobset}->name]});
|
||||
$c->stash->{channelBaseName} =
|
||||
|
@ -230,31 +153,8 @@ sub edit : Chained('jobsetChain') PathPart Args(0) {
|
|||
|
||||
$c->stash->{template} = 'edit-jobset.tt';
|
||||
$c->stash->{edit} = 1;
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
$c->stash->{clone} = defined $c->stash->{params}->{clone};
|
||||
$c->stash->{totalShares} = getTotalShares($c->model('DB')->schema);
|
||||
}
|
||||
|
||||
|
||||
|
@ -263,10 +163,10 @@ sub nixExprPathFromParams {
|
|||
|
||||
# The Nix expression path must be relative and can't contain ".." elements.
|
||||
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"};
|
||||
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);
|
||||
}
|
||||
|
@ -275,7 +175,7 @@ sub nixExprPathFromParams {
|
|||
sub checkInputValue {
|
||||
my ($c, $type, $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");
|
||||
return $value;
|
||||
}
|
||||
|
@ -284,8 +184,11 @@ sub checkInputValue {
|
|||
sub updateJobset {
|
||||
my ($c, $jobset) = @_;
|
||||
|
||||
my $jobsetName = $c->stash->{jobsetName} // $jobset->name;
|
||||
error($c, "Invalid jobset name: ‘$jobsetName’") if $jobsetName !~ /^$jobsetNameRE$/;
|
||||
my $jobsetName = $c->stash->{params}->{name};
|
||||
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
|
||||
# build expression.
|
||||
|
@ -294,118 +197,61 @@ sub updateJobset {
|
|||
|
||||
my ($nixExprPath, $nixExprInput) = nixExprPathFromParams $c;
|
||||
|
||||
my $enabled = int($c->stash->{params}->{enabled});
|
||||
die if $enabled < 0 || $enabled > 2;
|
||||
|
||||
$jobset->update(
|
||||
{ name => $jobsetName
|
||||
, description => trim($c->stash->{params}->{"description"})
|
||||
, nixexprpath => $nixExprPath
|
||||
, nixexprinput => $nixExprInput
|
||||
, enabled => defined $c->stash->{params}->{enabled} ? 1 : 0
|
||||
, enabled => $enabled
|
||||
, enableemail => defined $c->stash->{params}->{enableemail} ? 1 : 0
|
||||
, emailoverride => trim($c->stash->{params}->{emailoverride}) || ""
|
||||
, 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}))
|
||||
, triggertime => $jobset->triggertime // time()
|
||||
, triggertime => $enabled ? $jobset->triggertime // time() : undef
|
||||
, schedulingshares => int($c->stash->{params}->{schedulingshares})
|
||||
});
|
||||
|
||||
# Process the inputs of this jobset.
|
||||
unless (defined $c->stash->{params}->{inputs}) {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
# Set the inputs of this jobset.
|
||||
$jobset->jobsetinputs->delete;
|
||||
|
||||
foreach my $inputName (keys %{$c->stash->{params}->{inputs}}) {
|
||||
my $inputData = $c->stash->{params}->{inputs}->{$inputName};
|
||||
error($c, "Invalid input name: $inputName") unless $inputName =~ /^[[:alpha:]]\w*$/;
|
||||
|
||||
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;
|
||||
foreach my $name (keys %{$c->stash->{params}->{inputs}}) {
|
||||
my $inputData = $c->stash->{params}->{inputs}->{$name};
|
||||
my $type = $inputData->{type};
|
||||
my $values = $inputData->{values};
|
||||
$values = [] unless defined $values;
|
||||
$values = [$values] unless ref($values) eq 'ARRAY';
|
||||
my $emailresponsible = defined $inputData->{emailresponsible} ? 1 : 0;
|
||||
|
||||
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;
|
||||
foreach my $value (@{$values}) {
|
||||
$value = checkInputValue($c, $inputType, $value);
|
||||
foreach my $value (@values) {
|
||||
$value = checkInputValue($c, $type, $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) {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
my $jobset = $c->stash->{jobset};
|
||||
requireProjectOwner($c, $jobset->project);
|
||||
requireProjectOwner($c, $c->stash->{project});
|
||||
|
||||
$c->stash->{template} = 'clone-jobset.tt';
|
||||
}
|
||||
|
||||
|
||||
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]));
|
||||
$c->stash->{template} = 'edit-jobset.tt';
|
||||
$c->stash->{clone} = 1;
|
||||
$c->stash->{totalShares} = getTotalShares($c->model('DB')->schema);
|
||||
}
|
||||
|
||||
|
||||
|
@ -428,24 +274,7 @@ sub evals_GET {
|
|||
my $offset = ($page - 1) * $resultsPerPage;
|
||||
$c->stash->{evals} = getEvals($self, $c, $evals, $offset, $resultsPerPage);
|
||||
my %entity = (
|
||||
evals => [ $evals->search({ 'me.hasnewbuilds' => 1 }, {
|
||||
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",
|
||||
}) ],
|
||||
evals => [ map { $_->{eval} } @{$c->stash->{evals}} ],
|
||||
first => "?page=1",
|
||||
last => "?page=" . POSIX::ceil($c->stash->{total}/$resultsPerPage)
|
||||
);
|
||||
|
|
|
@ -26,6 +26,9 @@ sub view : Chained('eval') PathPart('') Args(0) {
|
|||
|
||||
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 $eval2;
|
||||
|
||||
|
@ -36,6 +39,11 @@ sub view : Chained('eval') PathPart('') Args(0) {
|
|||
if ($compare =~ /^\d+$/) {
|
||||
$eval2 = $c->model('DB::JobsetEvals')->find($compare)
|
||||
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)$/) {
|
||||
my $j = $c->stash->{project}->jobsets->find({name => $compare})
|
||||
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;
|
||||
|
||||
my @builds = $eval->builds->search({}, { order_by => ["job", "system", "id"], columns => [@buildListColumns] });
|
||||
my @builds2 = defined $eval2
|
||||
? $eval2->builds->search({}, { order_by => ["job", "system", "id"], columns => [@buildListColumns] })
|
||||
: ();
|
||||
sub cmpBuilds {
|
||||
my ($a, $b) = @_;
|
||||
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->{stillFail} = [];
|
||||
|
@ -63,15 +78,19 @@ sub view : Chained('eval') PathPart('') Args(0) {
|
|||
$c->stash->{new} = [];
|
||||
$c->stash->{removed} = [];
|
||||
$c->stash->{unfinished} = [];
|
||||
$c->stash->{aborted} = [];
|
||||
|
||||
my $n = 0;
|
||||
foreach my $build (@builds) {
|
||||
if ($build->finished != 0 && ($build->buildstatus == 3 || $build->buildstatus == 4)) {
|
||||
push @{$c->stash->{aborted}}, $build;
|
||||
next;
|
||||
}
|
||||
my $d;
|
||||
my $found = 0;
|
||||
while ($n < scalar(@builds2)) {
|
||||
my $build2 = $builds2[$n];
|
||||
my $d = $build->get_column('job') cmp $build2->get_column('job')
|
||||
|| $build->get_column('system') cmp $build2->get_column('system');
|
||||
my $d = cmpBuilds($build, $build2);
|
||||
last if $d == -1;
|
||||
if ($d == 0) {
|
||||
$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.
|
||||
sub nix : Chained('eval') PathPart('channel') CaptureArgs(0) {
|
||||
my ($self, $c) = @_;
|
||||
|
@ -144,8 +182,20 @@ sub nix : Chained('eval') PathPart('channel') CaptureArgs(0) {
|
|||
->search({ finished => 1, buildstatus => 0 },
|
||||
{ columns => [@buildListColumns, 'drvpath', 'description', 'homepage']
|
||||
, join => ["buildoutputs"]
|
||||
, order_by => ["build.id", "buildoutputs.name"]
|
||||
, '+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,5 +1,6 @@
|
|||
package Hydra::Controller::Project;
|
||||
|
||||
use utf8;
|
||||
use strict;
|
||||
use warnings;
|
||||
use base 'Hydra::Base::Controller::ListBuilds';
|
||||
|
@ -9,35 +10,15 @@ use Hydra::Helper::CatalystUtils;
|
|||
|
||||
sub projectChain :Chained('/') :PathPart('project') :CaptureArgs(1) {
|
||||
my ($self, $c, $projectName) = @_;
|
||||
$c->stash->{params}->{name} //= $projectName;
|
||||
|
||||
my $project = $c->model('DB::Projects')->find($projectName, { columns => [
|
||||
"me.name",
|
||||
"me.displayName",
|
||||
"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 });
|
||||
$c->stash->{project} = $c->model('DB::Projects')->find($projectName, {
|
||||
join => [ 'releases' ],
|
||||
order_by => { -desc => "releases.timestamp" },
|
||||
});
|
||||
|
||||
if ($project) {
|
||||
$c->stash->{project} = $project;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
notFound($c, "Project ‘$projectName’ doesn't exist.")
|
||||
if !$c->stash->{project} && !($c->action->name eq "project" and $c->request->method eq "PUT");
|
||||
}
|
||||
|
||||
|
||||
|
@ -53,55 +34,27 @@ sub project_GET {
|
|||
$c->stash->{releases} = [$c->stash->{project}->releases->search({},
|
||||
{order_by => ["timestamp DESC"]})];
|
||||
|
||||
$self->status_ok(
|
||||
$c,
|
||||
entity => $c->stash->{project}
|
||||
);
|
||||
$self->status_ok($c, entity => $c->stash->{project});
|
||||
}
|
||||
|
||||
sub project_PUT {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
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});
|
||||
|
||||
txn_do($c->model('DB')->schema, sub {
|
||||
updateProject($c, $c->stash->{project});
|
||||
});
|
||||
|
||||
if ($c->req->looks_like_browser) {
|
||||
$c->res->redirect($c->uri_for($self->action_for("project"), [$c->stash->{project}->name]) . "#tabs-configuration");
|
||||
} 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"), [$c->stash->{project}->name]) . "#tabs-configuration";
|
||||
$self->status_ok($c, entity => { redirect => "$uri" });
|
||||
|
||||
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) {
|
||||
$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 {
|
||||
else {
|
||||
requireMayCreateProjects($c);
|
||||
error($c, "Invalid project name: ‘$c->stash->{projectName}’") if $c->stash->{projectName} !~ /^$projectNameRE$/;
|
||||
|
||||
my $project;
|
||||
txn_do($c->model('DB')->schema, sub {
|
||||
|
@ -110,23 +63,34 @@ sub project_PUT {
|
|||
# valid. Idem for the owner.
|
||||
my $owner = $c->user->username;
|
||||
$project = $c->model('DB::Projects')->create(
|
||||
{name => $c->stash->{projectName}, displayname => "", owner => $owner});
|
||||
{ name => ".tmp", displayname => "", owner => $owner });
|
||||
updateProject($c, $project);
|
||||
});
|
||||
|
||||
my $uri = $c->uri_for($self->action_for("project"), [$project->name]);
|
||||
if ($c->req->looks_like_browser) {
|
||||
$c->res->redirect($uri . "#tabs-configuration");
|
||||
} else {
|
||||
$self->status_created(
|
||||
$c,
|
||||
$self->status_created($c,
|
||||
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) {
|
||||
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 {
|
||||
my ($c) = @_;
|
||||
|
||||
requireLogin($c) if !$c->user_exists;
|
||||
|
||||
error($c, "Only administrators or authorised users can perform this operation.")
|
||||
requireUser($c);
|
||||
accessDenied($c, "Only administrators or authorised users can perform this operation.")
|
||||
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) {
|
||||
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->{create} = 1;
|
||||
$c->stash->{edit} = 1;
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
$c->stash->{totalShares} = getTotalShares($c->model('DB')->schema);
|
||||
}
|
||||
|
||||
|
||||
|
@ -218,15 +139,18 @@ sub updateProject {
|
|||
my $owner = $project->owner;
|
||||
if ($c->check_user_roles('admin') and defined $c->stash->{params}->{owner}) {
|
||||
$owner = trim $c->stash->{params}->{owner};
|
||||
error($c, "Invalid owner: $owner")
|
||||
unless defined $c->model('DB::Users')->find({username => $owner});
|
||||
error($c, "The user name ‘$owner’ does not exist.")
|
||||
unless defined $c->model('DB::Users')->find($owner);
|
||||
}
|
||||
|
||||
my $projectName = $c->stash->{projectName} or $project->name;
|
||||
error($c, "Invalid project name: ‘$projectName’") if $projectName !~ /^$projectNameRE$/;
|
||||
my $projectName = $c->stash->{params}->{name};
|
||||
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};
|
||||
error($c, "Invalid display name: $displayName") if $displayName eq "";
|
||||
error($c, "You must specify a display name.") if $displayName eq "";
|
||||
|
||||
$project->update(
|
||||
{ name => $projectName
|
||||
|
@ -244,10 +168,6 @@ sub updateProject {
|
|||
sub get_builds : Chained('projectChain') PathPart('') CaptureArgs(0) {
|
||||
my ($self, $c) = @_;
|
||||
$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')
|
||||
->search({}, {bind => [$c->stash->{project}->name]});
|
||||
$c->stash->{channelBaseName} = $c->stash->{project}->name;
|
||||
|
|
|
@ -38,7 +38,7 @@ sub updateRelease {
|
|||
, description => trim $c->request->params->{description}
|
||||
});
|
||||
|
||||
$release->releasemembers->delete_all;
|
||||
$release->releasemembers->delete;
|
||||
foreach my $param (keys %{$c->request->params}) {
|
||||
next unless $param =~ /^member-(\d+)-description$/;
|
||||
my $buildId = $1;
|
||||
|
@ -72,7 +72,7 @@ sub submit : Chained('release') PathPart('submit') Args(0) {
|
|||
txn_do($c->model('DB')->schema, sub {
|
||||
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]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use Hydra::Helper::CatalystUtils;
|
|||
use Digest::SHA1 qw(sha1_hex);
|
||||
use Nix::Store;
|
||||
use Nix::Config;
|
||||
use Encode;
|
||||
|
||||
# Put this controller at top-level.
|
||||
__PACKAGE__->config->{namespace} = '';
|
||||
|
@ -33,6 +34,7 @@ sub begin :Private {
|
|||
$c->stash->{inputTypes} = {
|
||||
'string' => 'String value',
|
||||
'boolean' => 'Boolean',
|
||||
'nix' => 'Nix expression',
|
||||
'build' => 'Build output',
|
||||
'sysbuild' => 'Build output (same system)'
|
||||
};
|
||||
|
@ -54,12 +56,8 @@ sub index :Path :Args(0) {
|
|||
$c->stash->{template} = 'overview.tt';
|
||||
$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 })];
|
||||
$self->status_ok(
|
||||
$c,
|
||||
entity => [$c->model('DB::Projects')->search(isAdmin($c) ? {} : {hidden => 0}, {
|
||||
order_by => 'name',
|
||||
columns => [ 'name', 'displayname' ]
|
||||
})]
|
||||
$self->status_ok($c,
|
||||
entity => $c->stash->{projects}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -72,8 +70,7 @@ sub queue_GET {
|
|||
$c->stash->{flashMsg} //= $c->flash->{buildMsg};
|
||||
$self->status_ok(
|
||||
$c,
|
||||
entity => [$c->model('DB::Builds')->search(
|
||||
{finished => 0}, { join => ['project'], order_by => ["priority DESC", "id"], columns => [@buildListColumns], '+select' => ['project.enabled'], '+as' => ['enabled'] })]
|
||||
entity => [$c->model('DB::Builds')->search({finished => 0}, { order_by => ["priority DESC", "id"]})]
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -100,22 +97,7 @@ sub status_GET {
|
|||
$c,
|
||||
entity => [ $c->model('DB::BuildSteps')->search(
|
||||
{ 'me.busy' => 1, 'build.finished' => 0, 'build.busy' => 1 },
|
||||
{ join => { build => [ 'project', 'job', 'jobset' ] },
|
||||
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' ]
|
||||
}
|
||||
{ order_by => [ 'machine' ], join => [ 'build' ] }
|
||||
) ]
|
||||
);
|
||||
}
|
||||
|
@ -150,11 +132,9 @@ sub machines :Local Args(0) {
|
|||
sub get_builds : Chained('/') PathPart('') CaptureArgs(0) {
|
||||
my ($self, $c) = @_;
|
||||
$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->{channelBaseName} = "everything";
|
||||
$c->stash->{total} = $c->model('DB::NrBuilds')->find('finished')->count;
|
||||
}
|
||||
|
||||
|
||||
|
@ -213,35 +193,32 @@ sub default :Path {
|
|||
sub end : ActionClass('RenderView') {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
my @errors = map { encode_utf8($_); } @{$c->error};
|
||||
|
||||
if (defined $c->stash->{json}) {
|
||||
if (scalar @{$c->error}) {
|
||||
$c->stash->{json}->{error} = join "\n", @{$c->error};
|
||||
if (scalar @errors) {
|
||||
$c->stash->{json}->{error} = join "\n", @errors;
|
||||
$c->clear_errors;
|
||||
}
|
||||
$c->forward('View::JSON');
|
||||
}
|
||||
|
||||
if (scalar @{$c->error}) {
|
||||
$c->stash->{resource} = { errors => "$c->error" };
|
||||
elsif (scalar @{$c->error}) {
|
||||
$c->stash->{resource} = { error => join "\n", @{$c->error} };
|
||||
$c->stash->{template} = 'error.tt';
|
||||
$c->stash->{errors} = $c->error;
|
||||
$c->stash->{errors} = [@errors];
|
||||
$c->response->status(500) if $c->response->status == 200;
|
||||
if ($c->response->status >= 300) {
|
||||
$c->stash->{httpStatus} =
|
||||
$c->response->status . " " . HTTP::Status::status_message($c->response->status);
|
||||
}
|
||||
$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') { }
|
||||
|
||||
|
||||
|
@ -282,6 +259,7 @@ sub narinfo :LocalRegex('^([a-z0-9]+).narinfo$') :Args(0) {
|
|||
my $path = queryPathFromHashPart($hash);
|
||||
|
||||
if (!$path) {
|
||||
$c->response->status(404);
|
||||
$c->response->content_type('text/plain');
|
||||
$c->stash->{plain}->{data} = "does not exist\n";
|
||||
$c->forward('Hydra::View::Plain');
|
||||
|
|
|
@ -182,15 +182,11 @@ sub currentUser :Path('/current-user') :ActionClass('REST') { }
|
|||
sub currentUser_GET {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
requireLogin($c) if !$c->user_exists;
|
||||
requireUser($c);
|
||||
|
||||
$self->status_ok(
|
||||
$c,
|
||||
entity => $c->model('DB::Users')->find({ 'me.username' => $c->user->username}, {
|
||||
columns => [ "me.fullname", "me.emailaddress", "me.username", "userroles.role" ]
|
||||
, join => [ "userroles" ]
|
||||
, collapse => 1
|
||||
})
|
||||
entity => $c->model("DB::Users")->find($c->user->username)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -198,9 +194,9 @@ sub currentUser_GET {
|
|||
sub user :Chained('/') PathPart('user') CaptureArgs(1) {
|
||||
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);
|
||||
|
||||
$c->stash->{user} = $c->model('DB::Users')->find($userName)
|
||||
|
@ -287,7 +283,7 @@ sub edit_POST {
|
|||
}
|
||||
|
||||
if (isAdmin($c)) {
|
||||
$user->userroles->delete_all;
|
||||
$user->userroles->delete;
|
||||
$user->userroles->create({ role => $_})
|
||||
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;
|
||||
|
|
|
@ -41,7 +41,7 @@ sub updateView {
|
|||
{ name => $viewName
|
||||
, description => trim $c->request->params->{description} });
|
||||
|
||||
$view->viewjobs->delete_all;
|
||||
$view->viewjobs->delete;
|
||||
|
||||
foreach my $param (keys %{$c->request->params}) {
|
||||
next unless $param =~ /^job-(\d+)-name$/;
|
||||
|
|
|
@ -2,6 +2,7 @@ package Hydra::Helper::AddBuilds;
|
|||
|
||||
use strict;
|
||||
use feature 'switch';
|
||||
use utf8;
|
||||
use XML::Simple;
|
||||
use IPC::Run;
|
||||
use Nix::Store;
|
||||
|
@ -15,6 +16,7 @@ use File::Path;
|
|||
use File::Temp;
|
||||
use File::Spec;
|
||||
use File::Slurp;
|
||||
use Hydra::Helper::PluginHooks;
|
||||
|
||||
our @ISA = qw(Exporter);
|
||||
our @EXPORT = qw(
|
||||
|
@ -86,10 +88,7 @@ sub fetchInputBuild {
|
|||
{ order_by => "me.id DESC", rows => 1
|
||||
, where => \ attrsToSQL($attrs, "me.id") });
|
||||
|
||||
if (!defined $prevBuild || !isValidPath(getMainOutput($prevBuild)->path)) {
|
||||
print STDERR "input `", $name, "': no previous build available\n";
|
||||
return undef;
|
||||
}
|
||||
return () if !defined $prevBuild || !isValidPath(getMainOutput($prevBuild)->path);
|
||||
|
||||
#print STDERR "input `", $name, "': using build ", $prevBuild->id, "\n";
|
||||
|
||||
|
@ -148,9 +147,8 @@ sub fetchInputSystemBuild {
|
|||
return @inputs;
|
||||
}
|
||||
|
||||
|
||||
sub fetchInput {
|
||||
my ($plugins, $db, $project, $jobset, $name, $type, $value) = @_;
|
||||
my ($plugins, $db, $project, $jobset, $name, $type, $value, $emailresponsible) = @_;
|
||||
my @inputs;
|
||||
|
||||
if ($type eq "build") {
|
||||
|
@ -159,7 +157,7 @@ sub fetchInput {
|
|||
elsif ($type eq "sysbuild") {
|
||||
@inputs = fetchInputSystemBuild($db, $project, $jobset, $name, $value);
|
||||
}
|
||||
elsif ($type eq "string") {
|
||||
elsif ($type eq "string" || $type eq "nix") {
|
||||
die unless defined $value;
|
||||
@inputs = { value => $value };
|
||||
}
|
||||
|
@ -170,7 +168,7 @@ sub fetchInput {
|
|||
else {
|
||||
my $found = 0;
|
||||
foreach my $plugin (@{$plugins}) {
|
||||
@inputs = $plugin->fetchInput($type, $name, $value);
|
||||
@inputs = $plugin->fetchInput($type, $name, $value, $project, $jobset);
|
||||
if (defined $inputs[0]) {
|
||||
$found = 1;
|
||||
last;
|
||||
|
@ -179,7 +177,10 @@ sub fetchInput {
|
|||
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;
|
||||
}
|
||||
|
@ -243,6 +244,9 @@ sub inputsToArgs {
|
|||
when ("boolean") {
|
||||
push @res, "--arg", $input, booleanToString($exprType, $alt->{value});
|
||||
}
|
||||
when ("nix") {
|
||||
push @res, "--arg", $input, $alt->{value};
|
||||
}
|
||||
default {
|
||||
push @res, "--arg", $input, buildInputToString($exprType, $alt);
|
||||
}
|
||||
|
@ -287,17 +291,25 @@ sub evalJobs {
|
|||
my $validJob = 1;
|
||||
foreach my $arg (@{$job->{arg}}) {
|
||||
my $input = $inputInfo->{$arg->{name}}->[$arg->{altnr}];
|
||||
if ($input->{type} eq "sysbuild" && $input->{system} ne $job->{system}) {
|
||||
$validJob = 0;
|
||||
}
|
||||
}
|
||||
if ($validJob) {
|
||||
push(@filteredJobs, $job);
|
||||
$validJob = 0 if $input->{type} eq "sysbuild" && $input->{system} ne $job->{system};
|
||||
}
|
||||
push(@filteredJobs, $job) if $validJob;
|
||||
}
|
||||
$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.
|
||||
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}};
|
||||
die unless scalar @outputNames;
|
||||
|
@ -410,9 +422,7 @@ sub checkBuild {
|
|||
my $build;
|
||||
|
||||
txn_do($db, sub {
|
||||
my $job = $jobset->jobs->update_or_create(
|
||||
{ name => $jobName
|
||||
});
|
||||
my $job = $jobset->jobs->update_or_create({ name => $jobName });
|
||||
|
||||
# Don't add a build that has already been scheduled for this
|
||||
# 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
|
||||
# the eval), but they give a factor 1000 speedup on
|
||||
# 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 },
|
||||
{ rows => 1, columns => ['id'], join => ['buildoutputs'] });
|
||||
if (defined $prevBuild) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
# Prevent multiple builds with the same (job, outPath) from
|
||||
# being added.
|
||||
my $prev = $$jobOutPathMap{$job->name . "\t" . $firstOutputPath};
|
||||
my $prev = $$jobOutPathMap{$jobName . "\t" . $firstOutputPath};
|
||||
if (defined $prev) {
|
||||
print STDERR " already scheduled as build ", $prev, "\n";
|
||||
return;
|
||||
|
@ -511,12 +521,13 @@ sub checkBuild {
|
|||
$build->buildoutputs->create({ name => $_, path => $buildInfo->{output}->{$_}->{path} })
|
||||
foreach @outputNames;
|
||||
|
||||
$buildIds->{$build->id} = 1;
|
||||
$$jobOutPathMap{$job->name . "\t" . $firstOutputPath} = $build->id;
|
||||
$buildMap->{$build->id} = { id => $build->id, jobName => $jobName, new => 1, drvPath => $drvPath };
|
||||
$$jobOutPathMap{$jobName . "\t" . $firstOutputPath} = $build->id;
|
||||
|
||||
if ($build->iscachedbuild) {
|
||||
print STDERR " marked as cached build ", $build->id, "\n";
|
||||
addBuildProducts($db, $build);
|
||||
notifyBuildFinished($plugins, $build, []);
|
||||
} else {
|
||||
print STDERR " added to queue as build ", $build->id, "\n";
|
||||
}
|
||||
|
@ -545,6 +556,7 @@ sub checkBuild {
|
|||
, uri => $input->{uri}
|
||||
, revision => $input->{revision}
|
||||
, value => $input->{value}
|
||||
, emailresponsible => $input->{emailresponsible}
|
||||
, dependency => $input->{id}
|
||||
, path => $input->{storePath} || "" # !!! temporary hack
|
||||
, sha256hash => $input->{sha256hash}
|
||||
|
@ -556,29 +568,4 @@ sub checkBuild {
|
|||
};
|
||||
|
||||
|
||||
sub restartBuild {
|
||||
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});
|
||||
}
|
||||
});
|
||||
}
|
||||
1;
|
||||
|
|
|
@ -15,8 +15,8 @@ use feature qw/switch/;
|
|||
our @ISA = qw(Exporter);
|
||||
our @EXPORT = qw(
|
||||
getBuild getPreviousBuild getNextBuild getPreviousSuccessfulBuild
|
||||
error notFound
|
||||
requireLogin requireProjectOwner requireAdmin requirePost isAdmin isProjectOwner
|
||||
error notFound accessDenied
|
||||
forceLogin requireUser requireProjectOwner requireAdmin requirePost isAdmin isProjectOwner
|
||||
trim
|
||||
getLatestFinishedEval
|
||||
sendEmail
|
||||
|
@ -27,6 +27,7 @@ our @EXPORT = qw(
|
|||
parseJobsetName
|
||||
showJobName
|
||||
showStatus
|
||||
getResponsibleAuthors
|
||||
);
|
||||
|
||||
|
||||
|
@ -102,6 +103,12 @@ sub notFound {
|
|||
}
|
||||
|
||||
|
||||
sub accessDenied {
|
||||
my ($c, $msg) = @_;
|
||||
error($c, $msg, 403);
|
||||
}
|
||||
|
||||
|
||||
sub backToReferer {
|
||||
my ($c) = @_;
|
||||
$c->response->redirect($c->session->{referer} || $c->uri_for('/'));
|
||||
|
@ -110,26 +117,33 @@ sub backToReferer {
|
|||
}
|
||||
|
||||
|
||||
sub requireLogin {
|
||||
sub forceLogin {
|
||||
my ($c) = @_;
|
||||
$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 {
|
||||
my ($c, $project) = @_;
|
||||
|
||||
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 }));
|
||||
return
|
||||
$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 {
|
||||
my ($c, $project) = @_;
|
||||
|
||||
requireLogin($c) if !$c->user_exists;
|
||||
|
||||
error($c, "Only the project members or administrators can perform this operation.", 403)
|
||||
requireUser($c);
|
||||
accessDenied($c, "Only the project members or administrators can perform this operation.")
|
||||
unless isProjectOwner($c, $project);
|
||||
}
|
||||
|
||||
|
@ -142,8 +156,8 @@ sub isAdmin {
|
|||
|
||||
sub requireAdmin {
|
||||
my ($c) = @_;
|
||||
requireLogin($c) if !$c->user_exists;
|
||||
error($c, "Only administrators can perform this operation.", 403)
|
||||
requireUser($c);
|
||||
accessDenied($c, "Only administrators can perform this operation.")
|
||||
unless isAdmin($c);
|
||||
}
|
||||
|
||||
|
@ -206,12 +220,12 @@ sub paramToList {
|
|||
|
||||
|
||||
# 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 $relNameRE => "(?:[A-Za-z0-9-_][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 $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 $systemRE => "(?:[a-z0-9_]+-[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;
|
||||
|
|
|
@ -7,6 +7,7 @@ use File::Basename;
|
|||
use Config::General;
|
||||
use Hydra::Helper::CatalystUtils;
|
||||
use Hydra::Model::DB;
|
||||
use Nix::Store;
|
||||
|
||||
our @ISA = qw(Exporter);
|
||||
our @EXPORT = qw(
|
||||
|
@ -16,11 +17,13 @@ our @EXPORT = qw(
|
|||
getPrimaryBuildsForView
|
||||
getPrimaryBuildTotal
|
||||
getViewResult getLatestSuccessfulViewResult
|
||||
jobsetOverview removeAsciiEscapes getDrvLogPath logContents
|
||||
jobsetOverview removeAsciiEscapes getDrvLogPath findLog logContents
|
||||
getMainOutput
|
||||
getEvals getMachines
|
||||
pathIsInsidePrefix
|
||||
captureStdoutStderr);
|
||||
captureStdoutStderr run grab
|
||||
getTotalShares
|
||||
cancelBuilds restartBuilds);
|
||||
|
||||
|
||||
sub getHydraHome {
|
||||
|
@ -42,11 +45,12 @@ sub getHydraConfig {
|
|||
# doesn't work.
|
||||
sub txn_do {
|
||||
my ($db, $coderef) = @_;
|
||||
my $res;
|
||||
while (1) {
|
||||
eval {
|
||||
$db->txn_do($coderef);
|
||||
$res = $db->txn_do($coderef);
|
||||
};
|
||||
last if !$@;
|
||||
return $res if !$@;
|
||||
die $@ unless $@ =~ "database is locked";
|
||||
}
|
||||
}
|
||||
|
@ -253,21 +257,46 @@ sub getLatestSuccessfulViewResult {
|
|||
sub getDrvLogPath {
|
||||
my ($drvPath) = @_;
|
||||
my $base = basename $drvPath;
|
||||
my $fn =
|
||||
($ENV{NIX_LOG_DIR} || "/nix/var/log/nix") . "/drvs/"
|
||||
. substr($base, 0, 2) . "/"
|
||||
. substr($base, 2);
|
||||
return $fn if -f $fn;
|
||||
$fn .= ".bz2";
|
||||
return $fn if -f $fn;
|
||||
my $bucketed = substr($base, 0, 2) . "/" . substr($base, 2);
|
||||
my $fn = ($ENV{NIX_LOG_DIR} || "/nix/var/log/nix") . "/drvs/";
|
||||
for ($fn . $bucketed . ".bz2", $fn . $bucketed, $fn . $base . ".bz2", $fn . $base) {
|
||||
return $_ if (-f $_);
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
|
||||
# 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;
|
||||
}
|
||||
|
||||
|
||||
sub logContents {
|
||||
my ($drvPath, $tail) = @_;
|
||||
my $logPath = getDrvLogPath($drvPath);
|
||||
die unless defined $logPath;
|
||||
my ($logPath, $tail) = @_;
|
||||
my $cmd;
|
||||
if ($logPath =~ /.bz2$/) {
|
||||
$cmd = "bzip2 -d < $logPath";
|
||||
|
@ -381,7 +410,7 @@ sub getEvals {
|
|||
}
|
||||
|
||||
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.
|
||||
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;
|
||||
|
|
22
src/lib/Hydra/Helper/PluginHooks.pm
Normal file
22
src/lib/Hydra/Helper/PluginHooks.pm
Normal 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;
|
|
@ -38,7 +38,7 @@ sub supportedInputTypes {
|
|||
# Called to fetch an input of type ‘$type’. ‘$value’ is the input
|
||||
# location, typically the repository URL.
|
||||
sub fetchInput {
|
||||
my ($self, $type, $name, $value) = @_;
|
||||
my ($self, $type, $name, $value, $project, $jobset) = @_;
|
||||
return undef;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,21 +25,8 @@ sub fetchInput {
|
|||
|
||||
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.
|
||||
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);
|
||||
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";
|
||||
|
||||
(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;
|
||||
|
||||
($sha256, $storePath) = split ' ', $stdout;
|
||||
|
|
104
src/lib/Hydra/Plugin/DarcsInput.pm
Normal file
104
src/lib/Hydra/Plugin/DarcsInput.pm
Normal 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;
|
|
@ -28,6 +28,10 @@ The following dependent jobs also failed:
|
|||
* [% showJobName(b) %] ([% baseurl %]/build/[% b.id %])
|
||||
[% END -%]
|
||||
|
||||
[% END -%]
|
||||
[% IF nrCommits > 0 -%]
|
||||
This is likely due to [% IF nrCommits > 1 -%][% nrCommits %] commits by [% END -%][% authorList %].
|
||||
|
||||
[% END -%]
|
||||
[% IF build.buildstatus == 0 -%]
|
||||
Yay!
|
||||
|
@ -66,7 +70,7 @@ sub buildFinished {
|
|||
|
||||
my $to = $b->jobset->emailoverride ne "" ? $b->jobset->emailoverride : $b->maintainers;
|
||||
|
||||
foreach my $address (split ",", $to) {
|
||||
foreach my $address (split ",", ($to // "")) {
|
||||
$address = trim $address;
|
||||
|
||||
$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.
|
||||
# !!! should use the Template Toolkit here.
|
||||
|
||||
|
@ -89,6 +101,8 @@ sub buildFinished {
|
|||
, baseurl => $self->{config}->{'base_uri'} || "http://localhost:3000"
|
||||
, showJobName => \&showJobName, showStatus => \&showStatus
|
||||
, showSystem => index($build->job->name, $build->system) == -1
|
||||
, nrCommits => $nrCommits
|
||||
, authorList => $authorList
|
||||
};
|
||||
|
||||
my $body;
|
||||
|
|
|
@ -20,39 +20,34 @@ sub _cloneRepo {
|
|||
mkpath($cacheDir);
|
||||
my $clonePath = $cacheDir . "/" . sha256_hex($uri);
|
||||
|
||||
my $stdout = ""; my $stderr = ""; my $res;
|
||||
my $res;
|
||||
if (! -d $clonePath) {
|
||||
# Clone everything and fetch the branch.
|
||||
# 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);
|
||||
die "error cloning git repo at `$uri':\n$stderr" if $res;
|
||||
$res = run(cmd => ["git", "clone", "--branch", $branch, $uri, $clonePath], timeout => 600);
|
||||
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
|
||||
# the remote branch for whatever the repository state is. This command mirrors
|
||||
# only one branch of the remote repository.
|
||||
($res, $stdout, $stderr) = captureStdoutStderr(600,
|
||||
"git", "fetch", "-fu", "origin", "+$branch:$branch");
|
||||
($res, $stdout, $stderr) = captureStdoutStderr(600,
|
||||
"git", "fetch", "-fu", "origin") if $res;
|
||||
die "error fetching latest change from git repo at `$uri':\n$stderr" if $res;
|
||||
$res = run(cmd => ["git", "fetch", "-fu", "origin", "+$branch:$branch"], dir => $clonePath, timeout => 600);
|
||||
$res = run(cmd => ["git", "fetch", "-fu", "origin"], dir => $clonePath, timeout => 600) if $res->{status};
|
||||
die "error fetching latest change from git repo at `$uri':\n$res->{stderr}" if $res->{status};
|
||||
|
||||
# If deepClone is defined, then we look at the content of the repository
|
||||
# to determine if this is a top-git branch.
|
||||
if (defined $deepClone) {
|
||||
|
||||
# Checkout the branch to look at its content.
|
||||
($res, $stdout, $stderr) = captureStdoutStderr(600, "git", "checkout", "$branch");
|
||||
die "error checking out Git branch '$branch' at `$uri':\n$stderr" if $res;
|
||||
$res = run(cmd => ["git", "checkout", "$branch"], dir => $clonePath);
|
||||
die "error checking out Git branch '$branch' at `$uri':\n$res->{stderr}" if $res->{status};
|
||||
|
||||
if (-f ".topdeps") {
|
||||
# This is a TopGit branch. Fetch all the topic branches so
|
||||
# that builders can run "tg patch" and similar.
|
||||
($res, $stdout, $stderr) = captureStdoutStderr(600,
|
||||
"tg", "remote", "--populate", "origin");
|
||||
print STDERR "warning: `tg remote --populate origin' failed:\n$stderr" if $res;
|
||||
$res = run(cmd => ["tg", "remote", "--populate", "origin"], dir => $clonePath, timeout => 600);
|
||||
print STDERR "warning: `tg remote --populate origin' failed:\n$res->{stderr}" if $res->{status};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,7 +59,6 @@ sub _parseValue {
|
|||
(my $uri, my $branch, my $deepClone) = split ' ', $value;
|
||||
$branch = defined $branch ? $branch : "master";
|
||||
return ($uri, $branch, $deepClone);
|
||||
|
||||
}
|
||||
|
||||
sub fetchInput {
|
||||
|
@ -80,19 +74,13 @@ sub fetchInput {
|
|||
my $sha256;
|
||||
my $storePath;
|
||||
|
||||
my ($res, $stdout, $stderr) = captureStdoutStderr(600,
|
||||
("git", "rev-parse", "$branch"));
|
||||
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"
|
||||
my $revision = grab(cmd => ["git", "rev-parse", "$branch"], dir => $clonePath, chomp => 1);
|
||||
die "did not get a well-formated revision number of Git branch '$branch' at `$uri'"
|
||||
unless $revision =~ /^[0-9a-fA-F]+$/;
|
||||
|
||||
my $ref = "refs/heads/$branch";
|
||||
|
||||
# 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.
|
||||
my $cachedInput ;
|
||||
my $cachedInput;
|
||||
($cachedInput) = $self->{db}->resultset('CachedGitInputs')->search(
|
||||
{uri => $uri, branch => $branch, revision => $revision},
|
||||
{rows => 1});
|
||||
|
@ -123,10 +111,7 @@ sub fetchInput {
|
|||
$ENV{"NIX_PREFETCH_GIT_DEEP_CLONE"} = "1";
|
||||
}
|
||||
|
||||
($res, $stdout, $stderr) = captureStdoutStderr(600, "nix-prefetch-git", $clonePath, $revision);
|
||||
die "cannot check out Git repository branch '$branch' at `$uri':\n$stderr" if $res;
|
||||
|
||||
($sha256, $storePath) = split ' ', $stdout;
|
||||
($sha256, $storePath) = split ' ', grab(cmd => ["nix-prefetch-git", $clonePath, $revision], chomp => 1);
|
||||
|
||||
txn_do($self->{db}, sub {
|
||||
$self->{db}->resultset('CachedGitInputs')->update_or_create(
|
||||
|
@ -143,12 +128,9 @@ sub fetchInput {
|
|||
# number of commits in the history of this revision (‘revCount’)
|
||||
# the output of git-describe (‘gitTag’), and the abbreviated
|
||||
# revision (‘shortRev’).
|
||||
my $revCount = `git rev-list $revision | wc -l`; chomp $revCount;
|
||||
die "git rev-list failed" if $? != 0;
|
||||
my $gitTag = `git describe --always $revision`; chomp $gitTag;
|
||||
die "git describe failed" if $? != 0;
|
||||
my $shortRev = `git rev-parse --short $revision`; chomp $shortRev;
|
||||
die "git rev-parse failed" if $? != 0;
|
||||
my $revCount = scalar(split '\n', grab(cmd => ["git", "rev-list", "$revision"], dir => $clonePath));
|
||||
my $gitTag = grab(cmd => ["git", "describe", "--always", "$revision"], dir => $clonePath, chomp => 1);
|
||||
my $shortRev = grab(cmd => ["git", "rev-parse", "--short", "$revision"], dir => $clonePath, chomp => 1);
|
||||
|
||||
return
|
||||
{ uri => $uri
|
||||
|
@ -172,9 +154,7 @@ sub getCommits {
|
|||
|
||||
my $clonePath = $self->_cloneRepo($uri, $branch, $deepClone);
|
||||
|
||||
my $out;
|
||||
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 $out = grab(cmd => ["git", "log", "--pretty=format:%H%x09%an%x09%ae%x09%at", "$rev1..$rev2"], dir => $clonePath);
|
||||
|
||||
my $res = [];
|
||||
foreach my $line (split /\n/, $out) {
|
||||
|
|
|
@ -9,7 +9,7 @@ sub buildFinished {
|
|||
my ($self, $build, $dependents) = @_;
|
||||
|
||||
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";
|
||||
|
||||
|
@ -37,33 +37,7 @@ sub buildFinished {
|
|||
|
||||
return if scalar keys %rooms == 0;
|
||||
|
||||
# Determine who broke/fixed the build.
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
my ($authors, $nrCommits) = getResponsibleAuthors($build, $self->{plugins});
|
||||
|
||||
# Send a message to each room.
|
||||
foreach my $roomId (keys %rooms) {
|
||||
|
@ -83,16 +57,15 @@ sub buildFinished {
|
|||
$msg .= " (and ${\scalar @deps} others)" if scalar @deps > 0;
|
||||
$msg .= ": <a href='$baseurl/build/${\$build->id}'>" . showStatus($build) . "</a>";
|
||||
|
||||
if (scalar keys %authors > 0) {
|
||||
if (scalar keys %{$authors} > 0) {
|
||||
# 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 .= "$nrCommits commits by " if $nrCommits > 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";
|
||||
next;
|
||||
|
||||
my $ua = LWP::UserAgent->new();
|
||||
my $resp = $ua->post('https://api.hipchat.com/v1/rooms/message?format=json&auth_token=' . $room->{room}->{token}, {
|
||||
|
|
|
@ -12,21 +12,33 @@ sub supportedInputTypes {
|
|||
$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 {
|
||||
my ($self, $type, $name, $value) = @_;
|
||||
|
||||
return undef if $type ne "hg";
|
||||
|
||||
(my $uri, my $id) = split ' ', $value;
|
||||
(my $uri, my $id) = _parseValue($value);
|
||||
$id = defined $id ? $id : "default";
|
||||
|
||||
# init local hg clone
|
||||
|
||||
my $stdout = ""; my $stderr = "";
|
||||
|
||||
my $cacheDir = getSCMCacheDir . "/hg";
|
||||
mkpath($cacheDir);
|
||||
my $clonePath = $cacheDir . "/" . sha256_hex($uri);
|
||||
my $clonePath = _clonePath($uri);
|
||||
|
||||
if (! -d $clonePath) {
|
||||
(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;
|
||||
|
|
|
@ -34,8 +34,13 @@ sub fetchInput {
|
|||
} else {
|
||||
|
||||
print STDERR "copying input ", $name, " from $uri\n";
|
||||
if ( $uri =~ /^\// ) {
|
||||
$storePath = `nix-store --add "$uri"`
|
||||
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;
|
||||
|
||||
$sha256 = (queryPathInfo($storePath, 0))[1] or die;
|
||||
|
|
149
src/lib/Hydra/Plugin/S3Backup.pm
Normal file
149
src/lib/Hydra/Plugin/S3Backup.pm
Normal 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;
|
111
src/lib/Hydra/Schema/AggregateConstituents.pm
Normal file
111
src/lib/Hydra/Schema/AggregateConstituents.pm
Normal 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;
|
|
@ -72,6 +72,12 @@ __PACKAGE__->table("BuildInputs");
|
|||
data_type: 'text'
|
||||
is_nullable: 1
|
||||
|
||||
=head2 emailresponsible
|
||||
|
||||
data_type: 'integer'
|
||||
default_value: 0
|
||||
is_nullable: 0
|
||||
|
||||
=head2 dependency
|
||||
|
||||
data_type: 'integer'
|
||||
|
@ -105,6 +111,8 @@ __PACKAGE__->add_columns(
|
|||
{ data_type => "text", is_nullable => 1 },
|
||||
"value",
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
"emailresponsible",
|
||||
{ data_type => "integer", default_value => 0, is_nullable => 0 },
|
||||
"dependency",
|
||||
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
|
||||
"path",
|
||||
|
@ -168,7 +176,7 @@ __PACKAGE__->belongs_to(
|
|||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tKZAybbNaRIMs9n5tHkqPw
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-08 13:08:15
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OaJPzRM+8XGsu3eIkqeYEw
|
||||
|
||||
1;
|
||||
|
|
|
@ -97,6 +97,14 @@ __PACKAGE__->belongs_to(
|
|||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
|
||||
# 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;
|
||||
|
|
|
@ -169,4 +169,21 @@ __PACKAGE__->has_many(
|
|||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
|
||||
# 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;
|
||||
|
|
|
@ -288,6 +288,36 @@ __PACKAGE__->set_primary_key("id");
|
|||
|
||||
=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
|
||||
|
||||
Type: has_many
|
||||
|
@ -468,9 +498,37 @@ __PACKAGE__->has_many(
|
|||
undef,
|
||||
);
|
||||
|
||||
=head2 aggregates
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:isCEXACY/PwkvgKHcXvAIg
|
||||
Type: many_to_many
|
||||
|
||||
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(
|
||||
"dependents",
|
||||
|
@ -502,6 +560,8 @@ __PACKAGE__->has_many(
|
|||
|
||||
__PACKAGE__->many_to_many("jobsetevals", "jobsetevalmembers", "eval");
|
||||
|
||||
__PACKAGE__->many_to_many("constituents_", "aggregateconstituents_aggregates", "constituent");
|
||||
|
||||
sub makeSource {
|
||||
my ($name, $query) = @_;
|
||||
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)";
|
||||
|
||||
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(
|
||||
"LatestSucceeded$name",
|
||||
<<QUERY
|
||||
|
@ -571,4 +601,30 @@ makeQueries('ForJobset', "and project = ? and jobset = ?");
|
|||
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;
|
||||
|
|
98
src/lib/Hydra/Schema/CachedDarcsInputs.pm
Normal file
98
src/lib/Hydra/Schema/CachedDarcsInputs.pm
Normal 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;
|
|
@ -137,8 +137,27 @@ __PACKAGE__->belongs_to(
|
|||
{ 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
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t2CCfUjFEz/lO4szROz1AQ
|
||||
Type: has_many
|
||||
|
||||
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;
|
||||
|
|
|
@ -169,6 +169,16 @@ __PACKAGE__->belongs_to(
|
|||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
|
||||
# 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;
|
||||
|
|
|
@ -199,4 +199,22 @@ __PACKAGE__->has_many(
|
|||
|
||||
__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;
|
||||
|
|
|
@ -57,6 +57,12 @@ __PACKAGE__->table("JobsetInputs");
|
|||
data_type: 'text'
|
||||
is_nullable: 0
|
||||
|
||||
=head2 emailresponsible
|
||||
|
||||
data_type: 'integer'
|
||||
default_value: 0
|
||||
is_nullable: 0
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
|
@ -68,6 +74,8 @@ __PACKAGE__->add_columns(
|
|||
{ data_type => "text", is_nullable => 0 },
|
||||
"type",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
"emailresponsible",
|
||||
{ data_type => "integer", default_value => 0, is_nullable => 0 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
|
@ -142,7 +150,17 @@ __PACKAGE__->has_many(
|
|||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:UXBzqO0vHPql4LYyXpgEQg
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-08 13:06:15
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:+mZZqLjQNwblb/EWW1alLQ
|
||||
|
||||
my %hint = (
|
||||
relations => {
|
||||
"jobsetinputalts" => "value"
|
||||
}
|
||||
);
|
||||
|
||||
sub json_hint {
|
||||
return \%hint;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -118,6 +118,17 @@ __PACKAGE__->table("Jobsets");
|
|||
default_value: 300
|
||||
is_nullable: 0
|
||||
|
||||
=head2 schedulingshares
|
||||
|
||||
data_type: 'integer'
|
||||
default_value: 100
|
||||
is_nullable: 0
|
||||
|
||||
=head2 fetcherrormsg
|
||||
|
||||
data_type: 'text'
|
||||
is_nullable: 1
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
|
@ -151,6 +162,10 @@ __PACKAGE__->add_columns(
|
|||
{ data_type => "integer", default_value => 3, is_nullable => 0 },
|
||||
"checkinterval",
|
||||
{ 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
|
||||
|
@ -271,8 +286,42 @@ __PACKAGE__->belongs_to(
|
|||
{ 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
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tsGR8MhZRIUeNwpcVczMUw
|
||||
Type: has_many
|
||||
|
||||
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;
|
||||
|
|
75
src/lib/Hydra/Schema/NrBuilds.pm
Normal file
75
src/lib/Hydra/Schema/NrBuilds.pm
Normal 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;
|
|
@ -226,6 +226,21 @@ __PACKAGE__->has_many(
|
|||
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
|
||||
|
||||
Type: has_many
|
||||
|
@ -267,15 +282,26 @@ Composing rels: L</projectmembers> -> username
|
|||
__PACKAGE__->many_to_many("usernames", "projectmembers", "username");
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:RffghAo9jAaqYk41y1Sdqw
|
||||
# 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
|
||||
# for you to hand-edit. If you do not either delete
|
||||
# this section or remove that file from @INC, this section
|
||||
# will be repeated redundantly when you re-create this
|
||||
# file again via Loader! See skip_load_external to disable
|
||||
# this feature.
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:PdNQ2mf5azBB6nI+iAm8fQ
|
||||
|
||||
my %hint = (
|
||||
columns => [
|
||||
"name",
|
||||
"displayname",
|
||||
"description",
|
||||
"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;
|
||||
|
|
161
src/lib/Hydra/Schema/StarredJobs.pm
Normal file
161
src/lib/Hydra/Schema/StarredJobs.pm
Normal 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;
|
|
@ -135,6 +135,21 @@ __PACKAGE__->has_many(
|
|||
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
|
||||
|
||||
Type: has_many
|
||||
|
@ -161,14 +176,22 @@ Composing rels: L</projectmembers> -> project
|
|||
__PACKAGE__->many_to_many("projects", "projectmembers", "project");
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-06-13 01:54:50
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hy3MKvFxfL+1bTc7Hcb1zA
|
||||
# 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
|
||||
# for you to hand-edit. If you do not either delete
|
||||
# this section or remove that file from @INC, this section
|
||||
# will be repeated redundantly when you re-create this
|
||||
# file again via Loader! See skip_load_external to disable
|
||||
# this feature.
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-10-14 15:46:29
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Hv9Ukqud0d3uIUot0ErKeg
|
||||
|
||||
my %hint = (
|
||||
columns => [
|
||||
"fullname",
|
||||
"emailaddress",
|
||||
"username"
|
||||
],
|
||||
relations => {
|
||||
userroles => "role"
|
||||
}
|
||||
);
|
||||
|
||||
sub json_hint {
|
||||
return \%hint;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -19,31 +19,83 @@ sub escape {
|
|||
sub process {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
my $res = "[\n";
|
||||
my %perSystem;
|
||||
|
||||
foreach my $pkg (@{$c->stash->{nixPkgs}}) {
|
||||
my $build = $pkg->{build};
|
||||
$res .= " # $pkg->{name}\n";
|
||||
$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";
|
||||
$perSystem{$build->system}->{$build->get_column('job')} = $pkg;
|
||||
}
|
||||
|
||||
$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;
|
||||
$tar->add_data("channel/channel-name", ($c->stash->{channelName} or "unnamed-channel"), {mtime => 0});
|
||||
$tar->add_data("channel/default.nix", $res, {mtime => 0});
|
||||
$tar->add_data("channel/channel-name", ($c->stash->{channelName} or "unnamed-channel"), {mtime => 1});
|
||||
$tar->add_data("channel/default.nix", $res, {mtime => 1});
|
||||
|
||||
my $tardata = $tar->write;
|
||||
my $bzip2data;
|
||||
|
|
|
@ -8,7 +8,7 @@ sub process {
|
|||
my ($self, $c) = @_;
|
||||
$c->response->content_encoding("utf-8");
|
||||
$c->response->content_type('text/plain') unless $c->response->content_type() ne "";
|
||||
$self->SUPER::process($c);
|
||||
$c->response->body($c->stash->{plain}->{data});
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -8,17 +8,18 @@ __PACKAGE__->config(
|
|||
TEMPLATE_EXTENSION => '.tt',
|
||||
PRE_CHOMP => 1,
|
||||
POST_CHOMP => 1,
|
||||
expose_methods => [qw/log_exists ellipsize/]);
|
||||
expose_methods => [qw/buildLogExists buildStepLogExists/]);
|
||||
|
||||
sub log_exists {
|
||||
my ($self, $c, $drvPath) = @_;
|
||||
my $x = getDrvLogPath($drvPath);
|
||||
return defined $x;
|
||||
sub buildLogExists {
|
||||
my ($self, $c, $build) = @_;
|
||||
my @outPaths = map { $_->path } $build->buildoutputs->all;
|
||||
return defined findLog($c, $build->drvpath, @outPaths);
|
||||
}
|
||||
|
||||
sub ellipsize {
|
||||
my ($self, $c, $s, $n) = @_;
|
||||
return length $s <= $n ? $s : substr($s, 0, $n - 3) . "...";
|
||||
sub buildStepLogExists {
|
||||
my ($self, $c, $step) = @_;
|
||||
my @outPaths = map { $_->path } $step->buildstepoutputs->all;
|
||||
return defined findLog($c, $step->drvpath, @outPaths);
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
[% project = build.project %]
|
||||
[% jobset = build.jobset %]
|
||||
[% job = build.job %]
|
||||
[% isAggregate = constituents.size > 0 %]
|
||||
|
||||
[% BLOCK renderOutputs %]
|
||||
[% start=1; FOREACH output IN outputs %]
|
||||
|
@ -22,7 +23,7 @@
|
|||
<tbody>
|
||||
[% FOREACH step IN build.buildsteps %]
|
||||
[% 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); %]
|
||||
<tr>
|
||||
<td>[% step.stepnr %]</td>
|
||||
|
@ -67,7 +68,40 @@
|
|||
[% END %]
|
||||
|
||||
<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>
|
||||
[% 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-buildinputs" data-toggle="tab">Inputs</a></li>
|
||||
[% 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">
|
||||
|
||||
[% 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>
|
||||
<tr>
|
||||
<td>
|
||||
|
@ -114,7 +128,28 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<th>System:</th>
|
||||
|
@ -146,7 +181,7 @@
|
|||
<td>[% IF cachedBuild; INCLUDE renderFullBuildLink build=cachedBuild; ELSE %]<em>unknown</em>[% END %]</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
[% IF build.finished %]
|
||||
[% IF !isAggregate && build.finished %]
|
||||
<tr>
|
||||
<th>Duration:</th>
|
||||
<td>[% actualBuild = build.iscachedbuild ? cachedBuild : build;
|
||||
|
@ -154,7 +189,7 @@
|
|||
finished at [% INCLUDE renderDateTime timestamp = actualBuild.stoptime %]</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
[% IF log_exists(build.drvpath) %]
|
||||
[% IF !isAggregate && buildLogExists(build) %]
|
||||
<tr>
|
||||
<th>Logfile:</th>
|
||||
<td>
|
||||
|
@ -169,20 +204,7 @@
|
|||
</tr>
|
||||
</table>
|
||||
|
||||
[% IF c.user_exists && available %]
|
||||
<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 %]
|
||||
[% IF build.buildproducts && !isAggregate %]
|
||||
|
||||
<h3>Build products</h3>
|
||||
|
||||
|
@ -251,6 +273,18 @@
|
|||
|
||||
</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">
|
||||
|
||||
<table class="info-table">
|
||||
|
@ -380,8 +414,8 @@
|
|||
<div id="placeholder" style="width:800px;height:400px;"></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="/static/js/flot/jquery.flot.selection.js" type="text/javascript"></script>
|
||||
<script src="[% c.uri_for("/static/js/flot/jquery.flot.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">
|
||||
$(function() {
|
||||
var d = [];
|
||||
|
@ -524,4 +558,58 @@
|
|||
</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">×</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 %]
|
||||
|
|
|
@ -60,7 +60,6 @@ install the package simply by clicking on the packages below.</p>
|
|||
[% ELSE %]
|
||||
[% HTML.escape(b.description) %]
|
||||
[% END %]
|
||||
[% IF pkg.outName != 'out' %] [[% pkg.outName %]][% END %]
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
|
|
@ -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 %]
|
|
@ -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 %]
|
|
@ -20,7 +20,7 @@ BLOCK renderJobsetName %]
|
|||
|
||||
|
||||
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;
|
||||
|
||||
|
||||
|
@ -40,9 +40,9 @@ END;
|
|||
|
||||
|
||||
BLOCK renderDuration;
|
||||
IF duration >= 24 * 60 * 60; duration div (24 * 60 * 60) %]d [% END;
|
||||
IF duration >= 60 * 60; duration div (60 * 60) % 24 %]h [% END;
|
||||
IF duration >= 60; duration div 60 % 60 %]m [% END;
|
||||
IF duration >= 24 * 60 * 60; duration div (24 * 60 * 60) %]d [% END;
|
||||
IF duration >= 60 * 60; duration div (60 * 60) % 24 %]h [% END;
|
||||
IF duration >= 60; duration div 60 % 60 %]m [% END;
|
||||
duration % 60 %]s[%
|
||||
END;
|
||||
|
||||
|
@ -64,12 +64,9 @@ BLOCK renderBuildListHeader %]
|
|||
[% IF !hideJobName %]
|
||||
<th>Job</th>
|
||||
[% END %]
|
||||
<th>Release Name</th>
|
||||
<th>Release name</th>
|
||||
<th>System</th>
|
||||
<th>[% IF showSchedulingInfo %]Queued at[% ELSE %]Finished at[% END %]</th>
|
||||
[% IF showStatusChange %]
|
||||
<th class="headerSortUp">Last status change</th>
|
||||
[% END %]
|
||||
[% IF showDescription %]
|
||||
<th>Description</th>
|
||||
[% END %]
|
||||
|
@ -99,25 +96,14 @@ BLOCK renderBuildListBody;
|
|||
[% END %]
|
||||
<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">[% date.format(showSchedulingInfo ? build.timestamp : build.stoptime, '%Y-%m-%d %H:%M:%S') %]</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 %]
|
||||
<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 showDescription %]
|
||||
<td>[% build.description %]</td>
|
||||
[% END %]
|
||||
</tr>
|
||||
[% END;
|
||||
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;
|
||||
|
||||
|
@ -144,7 +130,7 @@ END;
|
|||
|
||||
|
||||
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;
|
||||
|
||||
|
||||
|
@ -164,7 +150,7 @@ BLOCK renderSelection;
|
|||
[% ELSE %]
|
||||
<select [% HTML.attributes(id => param, name => param) %]>
|
||||
[% 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 %]
|
||||
</select>
|
||||
[% END;
|
||||
|
@ -195,24 +181,24 @@ BLOCK renderBuildStatusIcon;
|
|||
buildstatus = buildstatus != undef ? buildstatus : build.buildstatus;
|
||||
IF finished;
|
||||
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 %]
|
||||
<img src="/static/images/error_[% size %].png" alt="Failed" />
|
||||
[% ELSIF buildstatus == 2 %]
|
||||
<img src="/static/images/dependency_[% size %].png" alt="Dependency failed" />
|
||||
<img src="[% c.uri_for("/static/images/error_${size}.png") %]" alt="Failed" />
|
||||
[% ELSIF buildstatus == 2 || buildstatus == 5 %]
|
||||
<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 %]
|
||||
<img src="/static/images/cancelled_[% size %].png" alt="Cancelled" />
|
||||
[% ELSIF buildstatus == 5 %]
|
||||
<img src="/static/images/error_[% size %].png" alt="Failed" />
|
||||
<img src="[% c.uri_for("/static/images/forbidden_${size}.png") %]" alt="Cancelled" />
|
||||
[% 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 %]
|
||||
<img src="/static/images/error_[% size %].png" alt="Failed" />
|
||||
<img src="[% c.uri_for("/static/images/error_${size}.png") %]" alt="Failed" />
|
||||
[% END;
|
||||
ELSIF busy %]
|
||||
<img src="/static/images/help_[% size %].png" alt="Busy" />
|
||||
<img src="[% c.uri_for("/static/images/help_${size}.png") %]" alt="Busy" />
|
||||
[% ELSE %]
|
||||
<img src="/static/images/help_[% size %].png" alt="Scheduled" />
|
||||
<img src="[% c.uri_for("/static/images/help_${size}.png") %]" alt="Scheduled" />
|
||||
[% END;
|
||||
END;
|
||||
|
||||
|
@ -225,17 +211,15 @@ BLOCK renderStatus;
|
|||
<strong>Success</strong>
|
||||
[% ELSIF buildstatus == 1 %]
|
||||
<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>
|
||||
[% ELSIF buildstatus == 4 %]
|
||||
<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 %]
|
||||
<span class="error">Build failed (with result)</span>
|
||||
[% ELSE %]
|
||||
<span class="error">Build failed</span>
|
||||
(see <a href="#nix-error">below</a>)
|
||||
<span class="error">Aborted</span>
|
||||
(Hydra failure; see <a href="#nix-error">below</a>)
|
||||
[% END;
|
||||
ELSIF build.busy %]
|
||||
<strong>Build in progress</strong>
|
||||
|
@ -246,24 +230,15 @@ BLOCK renderStatus;
|
|||
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;
|
||||
IF input.type == "build" || input.type == "sysbuild" %]
|
||||
<a href="[% c.uri_for('/build' input.dependency.id) %]">[% input.dependency.id %]</a>
|
||||
[% ELSIF input.type == "string" || input.type == "boolean" %]
|
||||
<tt>"[% input.value %]"</tt>
|
||||
[% ELSIF input.type == "string" %]
|
||||
<tt>"[% HTML.escape(input.value) %]"</tt>
|
||||
[% ELSIF input.type == "nix" || input.type == "boolean" %]
|
||||
<tt>[% HTML.escape(input.value) %]</tt>
|
||||
[% 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;
|
||||
|
||||
|
@ -275,7 +250,7 @@ BLOCK renderDiffUri;
|
|||
url = bi1.uri;
|
||||
path = url.replace(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;
|
||||
END;
|
||||
END;
|
||||
|
@ -284,7 +259,7 @@ BLOCK renderDiffUri;
|
|||
url = res.0;
|
||||
branch = res.1;
|
||||
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;
|
||||
contents;
|
||||
END;
|
||||
|
@ -305,13 +280,15 @@ BLOCK renderInputs; %]
|
|||
<td>
|
||||
[% IF input.type == "build" || input.type == "sysbuild" %]
|
||||
[% INCLUDE renderFullBuildLink build=input.dependency %]
|
||||
[% ELSIF input.type == "string" || input.type == "boolean" %]
|
||||
<tt>"[% input.value %]"</tt>
|
||||
[% ELSIF input.type == "string" %]
|
||||
<tt>"[% HTML.escape(input.value) %]"</tt>
|
||||
[% ELSIF input.type == "nix" || input.type == "boolean" %]
|
||||
<tt>[% HTML.escape(input.value) %]</tt>
|
||||
[% ELSE %]
|
||||
<tt>[% input.uri %]</tt>
|
||||
<tt>[% HTML.escape(input.uri) %]</tt>
|
||||
[% END %]
|
||||
</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>
|
||||
</tr>
|
||||
[% END %]
|
||||
|
@ -372,10 +349,10 @@ BLOCK renderInputDiff; %]
|
|||
|
||||
BLOCK renderPager %]
|
||||
<ul class="pager">
|
||||
<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) %]">‹ Previous</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 »</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) %]">‹ Previous</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 »</a></li>
|
||||
</ul>
|
||||
[% END;
|
||||
|
||||
|
@ -458,22 +435,10 @@ BLOCK renderLogLinks %]
|
|||
|
||||
BLOCK makeLazyTab %]
|
||||
<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>
|
||||
<script>
|
||||
$(function() {
|
||||
$('.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>");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
$(function() { makeLazyTab("[% tabName %]", "[% uri %]"); });
|
||||
</script>
|
||||
[% END;
|
||||
|
||||
|
@ -485,4 +450,18 @@ BLOCK makePopover %]
|
|||
[% 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
42
src/root/dashboard.tt
Normal 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 %]
|
|
@ -12,7 +12,7 @@
|
|||
<span id="[% done.${node.path} %]"><span class="dep-tree-line">
|
||||
[% IF node.buildStep %]
|
||||
<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);
|
||||
END %]
|
||||
[% ELSE %]
|
||||
|
|
|
@ -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 %]
|
||||
[% USE format %]
|
||||
|
||||
[% BLOCK renderJobsetInputAlt %]
|
||||
<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 />
|
||||
[% END %]
|
||||
|
||||
|
@ -11,51 +12,61 @@
|
|||
<tr class="input [% extraClass %]" [% IF id %]id="[% id %]"[% END %]>
|
||||
<td>
|
||||
<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>
|
||||
[% INCLUDE renderSelection curValue=input.type param="$baseName-type" options=inputTypes %]
|
||||
</td>
|
||||
<td class="inputalts" id="[% baseName %]">
|
||||
[% FOREACH alt IN input.jobsetinputalts %]
|
||||
[% FOREACH alt IN input.search_related('jobsetinputalts', {}, { order_by => 'altnr' }) %]
|
||||
<span class="inputalt">
|
||||
[% INCLUDE renderJobsetInputAlt alt=alt name="$baseName-values" %]
|
||||
</span>
|
||||
[% 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>
|
||||
<input type="checkbox" id="[% baseName %]-emailresponsible" name="[% baseName %]-emailresponsible" [% IF input.emailresponsible; 'checked="checked"'; END %]/>
|
||||
</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
|
||||
[% BLOCK renderJobsetInputs %]
|
||||
<table class="table table-striped table-condensed">
|
||||
<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>
|
||||
<tbody class="inputs">
|
||||
[% FOREACH input IN jobset.jobsetinputs %]
|
||||
[% INCLUDE renderJobsetInput input=input baseName="input-$input.name" %]
|
||||
[% END %]
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
[% 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>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">State</label>
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" [% IF jobset.enabled; 'checked="checked"'; END %]></input>Enabled
|
||||
</label>
|
||||
<div class="btn-group" data-toggle="buttons-radio">
|
||||
<input type="hidden" name="enabled" value="[% jobset.enabled %]" />
|
||||
<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 class="control-group">
|
||||
<div class="controls">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -63,23 +74,23 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">Identifier</label>
|
||||
<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 class="control-group">
|
||||
<label class="control-label">Description</label>
|
||||
<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 class="control-group">
|
||||
<label class="control-label">Nix expression</label>
|
||||
<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
|
||||
<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>
|
||||
|
||||
|
@ -87,17 +98,29 @@
|
|||
<label class="control-label">Check interval</label>
|
||||
<div class="controls">
|
||||
<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>
|
||||
</div>
|
||||
<span class="help-inline">(0 to disable polling)</span>
|
||||
</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="controls">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -105,33 +128,21 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">Email override</label>
|
||||
<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 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">
|
||||
<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>
|
||||
|
||||
[% INCLUDE renderJobsetInputs %]
|
||||
|
||||
<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>
|
||||
|
||||
[% 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 %]
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
|
@ -144,7 +155,9 @@
|
|||
[% INCLUDE renderJobsetInputAlt alt=alt %]
|
||||
</tt>
|
||||
|
||||
<script type="text/javascript">
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
var id = 0;
|
||||
|
||||
|
@ -153,6 +166,7 @@
|
|||
var x = $("#input-template").clone(true).attr("id", "").insertBefore($(this).parents("tr")).show();
|
||||
$("#input-template-name", x).attr("name", newid + "-name");
|
||||
$("#input-template-type", x).attr("name", newid + "-type");
|
||||
$("#input-template-emailresponsible", x).attr("name", newid + "-emailresponsible");
|
||||
$("#input-template", x).attr("id", newid);
|
||||
return false;
|
||||
});
|
||||
|
@ -162,8 +176,47 @@
|
|||
$("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 %]
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
[% WRAPPER layout.tt title=(create ? "New project" : "Editing project $project.name") %]
|
||||
[% 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>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<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>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -21,58 +21,63 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">Identifier</label>
|
||||
<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 class="control-group">
|
||||
<label class="control-label">Display name</label>
|
||||
<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 class="control-group">
|
||||
<label class="control-label">Description</label>
|
||||
<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 class="control-group">
|
||||
<label class="control-label">Homepage</label>
|
||||
<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 class="control-group">
|
||||
<label class="control-label">Owner</label>
|
||||
<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 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>
|
||||
[%IF create %]Create[% ELSE %]Apply changes[% END %]
|
||||
[%IF create %]Create project[% ELSE %]Apply changes[% END %]
|
||||
</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>
|
||||
|
||||
</fieldset>
|
||||
|
||||
</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 %]
|
||||
|
|
|
@ -9,14 +9,14 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">Identifier</label>
|
||||
<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 class="control-group">
|
||||
<label class="control-label">Description</label>
|
||||
<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>
|
||||
|
||||
|
@ -30,7 +30,7 @@
|
|||
<div class="releaseMember control-group">
|
||||
<label class="control-label">Build [% m.build.id %] Label</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -26,14 +26,14 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">Identifier</label>
|
||||
<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 class="control-group">
|
||||
<label class="control-label">Description</label>
|
||||
<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>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
[% USE HTML %]
|
||||
|
||||
[% 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 %]
|
||||
|
|
|
@ -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 don’t evaluate properly and jobs
|
||||
that don’t 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 %]
|
|
@ -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 %]
|
||||
[% hideProjectName=1 hideJobsetName=1 hideJobName=1 %]
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
|
@ -12,7 +18,7 @@
|
|||
<div id="tabs-status" class="tab-pane active">
|
||||
[% IF lastBuilds.size != 0 %]
|
||||
<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') %]
|
||||
[% END %]
|
||||
[% IF queuedBuilds.size != 0 %]
|
||||
|
@ -21,12 +27,57 @@
|
|||
[% END %]
|
||||
</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">
|
||||
<ul>
|
||||
<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-for' system.system) %]">Latest successful build for <tt>[% system.system %]</tt></a></li>
|
||||
[% END %]
|
||||
<li><a href="[% c.uri_for('/job' project.name jobset.name job.name 'latest-finished') %]">Latest successful build from a finished evaluation</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -4,10 +4,14 @@
|
|||
<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>
|
||||
<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 %]
|
||||
<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>
|
||||
[% END; END %]
|
||||
[% END %]
|
||||
|
@ -19,81 +23,117 @@
|
|||
project=otherEval.jobset.project.name jobset=otherEval.jobset.name %] evaluation <a href="[%
|
||||
c.uri_for(c.controller('JobsetEval').action_for('view'),
|
||||
[otherEval.id]) %]">[% otherEval.id %]</a>.</p>
|
||||
[% ELSE %]
|
||||
<div class="alert">Couldn't find an evaluation to compare to.</div>
|
||||
[% 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">
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
<div id="tabs-status" class="tab-pane active">
|
||||
|
||||
[% BLOCK renderSome %]
|
||||
[% size = builds.size; max = full ? size : 30; %]
|
||||
[% BLOCK renderSome %]
|
||||
[% INCLUDE renderBuildListHeader unsortable=1 %]
|
||||
[% size = builds.size; max = full ? size : 250; %]
|
||||
[% INCLUDE renderBuildListBody builds=builds.slice(0, (size > max ? max : size) - 1)
|
||||
hideProjectName=1 hideJobsetName=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>
|
||||
[% END %]
|
||||
[% END %]
|
||||
[% INCLUDE renderBuildListFooter %]
|
||||
[% END %]
|
||||
|
||||
[% INCLUDE renderBuildListHeader unsortable=1 %]
|
||||
<div class="tab-content">
|
||||
|
||||
[% IF unfinished.size > 0 %]
|
||||
<tr><th class="subheader" colspan="6"><strong>Queued</strong> jobs</th></tr>
|
||||
[% INCLUDE renderSome builds=unfinished %]
|
||||
[% END %]
|
||||
<div id="tabs-aborted" class="tab-pane">
|
||||
[% INCLUDE renderSome builds=aborted %]
|
||||
</div>
|
||||
|
||||
[% IF new.size > 0 %]
|
||||
<tr><th class="subheader" colspan="6"><strong>New</strong> jobs</th></tr>
|
||||
<div id="tabs-now-fail" class="tab-pane">
|
||||
[% 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 %]
|
||||
[% END %]
|
||||
</div>
|
||||
|
||||
[% IF removed.size > 0 %]
|
||||
<tr><th class="subheader" colspan="6"><strong>Removed</strong> jobs</th></tr>
|
||||
[% size = removed.size; max = full ? size : 30; %]
|
||||
<div id="tabs-removed" class="tab-pane">
|
||||
<table class="table table-striped table-condensed clickable-rows">
|
||||
<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) %]
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
<td colspan="2">[% INCLUDE renderJobName project=project.name jobset=jobset.name job=j.job %]</td>
|
||||
<td colspan="2"><tt>[% j.system %]</tt></td>
|
||||
<td>[% INCLUDE renderJobName project=project.name jobset=jobset.name job=j.job %]</td>
|
||||
<td><tt>[% j.system %]</tt></td>
|
||||
</tr>
|
||||
[% END %]
|
||||
[% 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>
|
||||
[% END %]
|
||||
<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 %]
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
[% IF nowFail.size > 0 %]
|
||||
<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>
|
||||
<div id="tabs-still-fail" class="tab-pane">
|
||||
[% INCLUDE renderSome builds=stillFail %]
|
||||
[% END %]
|
||||
</div>
|
||||
|
||||
[% IF stillSucceed.size > 0 %]
|
||||
<tr><th class="subheader" colspan="6">Jobs that still <strong>succeed</strong></th></tr>
|
||||
<div id="tabs-still-succeed" class="tab-pane">
|
||||
[% INCLUDE renderSome builds=stillSucceed %]
|
||||
[% END %]
|
||||
|
||||
[% 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>
|
||||
|
||||
<div id="tabs-unfinished" class="tab-pane">
|
||||
[% INCLUDE renderSome builds=unfinished %]
|
||||
</div>
|
||||
|
||||
<div id="tabs-inputs" class="tab-pane">
|
||||
|
|
|
@ -1,15 +1,69 @@
|
|||
[% PROCESS common.tt %]
|
||||
[% PROCESS common.tt; USE Math %]
|
||||
|
||||
<p>This jobset currently contains the following [% activeJobs.size %] jobs:
|
||||
<blockquote>
|
||||
[% IF activeJobs.size == 0 %]<em>(none)</em>[% END %]
|
||||
[% FOREACH j IN activeJobs %][% INCLUDE renderJobName project=project.name jobset=jobset.name job=j %]<br/>[% END %]
|
||||
</blockquote>
|
||||
</p>
|
||||
<form class="form-search" id="filter-jobs">
|
||||
<div class="input-append">
|
||||
<input name="filter" type="text" class="input-large search-query" placeholder="Search jobs by name..." [% HTML.attributes(value => filter) %]/>
|
||||
<button type="button" class="btn btn-info [% IF showInactive %]active[% END %]" id="active-toggle">Show inactive jobs</button>
|
||||
</div>
|
||||
|
||||
<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:
|
||||
<blockquote>
|
||||
[% IF inactiveJobs.size == 0 %]<em>(none)</em>[% END %]
|
||||
[% FOREACH j IN inactiveJobs %][% INCLUDE renderJobName project=project.name jobset=jobset.name job=j %]<br/>[% END %]
|
||||
</blockquote>
|
||||
</p>
|
||||
<script>
|
||||
function setFilter(filter) {
|
||||
$('#filter-loading').show();
|
||||
if ($('#active-toggle').hasClass('active')) filter += '&showInactive=1';
|
||||
$('#tabs-jobs').load("[% c.uri_for('/jobset' project.name jobset.name "jobs-tab") %]", filter, function(response, status, xhr) {
|
||||
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 %]
|
||||
|
|
|
@ -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>
|
|
@ -1,5 +1,6 @@
|
|||
[% WRAPPER layout.tt title="Jobset $project.name:$jobset.name" %]
|
||||
[% PROCESS common.tt %]
|
||||
[% USE format %]
|
||||
|
||||
|
||||
[% BLOCK renderJobsetInput %]
|
||||
|
@ -11,7 +12,7 @@
|
|||
[% INCLUDE renderSelection curValue=input.type param="$baseName-type" options=inputTypes %]
|
||||
</td>
|
||||
<td class="inputalts" id="[% baseName %]">
|
||||
[% FOREACH alt IN input.jobsetinputalts %]
|
||||
[% FOREACH alt IN input.search_related('jobsetinputalts', {}, { order_by => 'altnr' }) %]
|
||||
<tt class="inputalt">
|
||||
[% IF input.type == "string" %]
|
||||
"[% HTML.escape(alt.value) %]"
|
||||
|
@ -41,11 +42,25 @@
|
|||
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a href="#tabs-evaluations" data-toggle="tab">Evaluations</a></li>
|
||||
[% IF jobset.errormsg %]
|
||||
<li><a href="#tabs-errors" data-toggle="tab"><img src="/static/images/error_16.png" /> Evaluation errors</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">
|
||||
[% 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 %]
|
||||
<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-configuration" data-toggle="tab">Configuration</a></li>
|
||||
</ul>
|
||||
|
@ -59,7 +74,7 @@
|
|||
<th>Last checked:</th>
|
||||
<td>
|
||||
[% 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 %]
|
||||
<em>never</em>
|
||||
[% END %]
|
||||
|
@ -91,20 +106,20 @@
|
|||
|
||||
</div>
|
||||
|
||||
[% INCLUDE makeLazyTab tabName="tabs-status" uri=c.uri_for('/jobset' project.name jobset.name "status-tab") %]
|
||||
|
||||
[% IF jobset.errormsg %]
|
||||
[% IF jobset.errormsg || jobset.fetcherrormsg %]
|
||||
<div id="tabs-errors" class="tab-pane">
|
||||
<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>
|
||||
[% END %]
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<th>Description:</th>
|
||||
<td>[% HTML.escape(jobset.description) %]</td>
|
||||
|
@ -116,14 +131,14 @@
|
|||
<tt>[% HTML.escape(jobset.nixexprinput) %]</tt>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Enabled:</th>
|
||||
<td>[% jobset.enabled ? "Yes" : "No" %]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Check interval:</th>
|
||||
<td>[% jobset.checkinterval || "<em>disabled</em>" %]</td>
|
||||
</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>
|
||||
<th>Enable email notification:</th>
|
||||
<td>[% jobset.enableemail ? "Yes" : "No" %]</td>
|
||||
|
@ -133,7 +148,7 @@
|
|||
<td>[% HTML.escape(jobset.emailoverride) %]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Number of builds to keep:</th>
|
||||
<th>Number of evaluations to keep:</th>
|
||||
<td>[% jobset.keepnr %]</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -145,4 +160,32 @@
|
|||
|
||||
</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 %]
|
||||
|
|
|
@ -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 %]
|
|
@ -1,9 +1,6 @@
|
|||
[% USE date %]
|
||||
[% USE HTML %]
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
[% PROCESS common.tt %]
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
|
@ -19,24 +16,25 @@
|
|||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<script type="text/javascript" src="/static/bootstrap/js/bootstrap.min.js"></script>
|
||||
<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<script type="text/javascript" src="[% c.uri_for("/static/bootstrap/js/bootstrap.min.js") %]"></script>
|
||||
<link href="[% c.uri_for("/static/bootstrap/css/bootstrap.min.css") %]" rel="stylesheet" />
|
||||
|
||||
<!-- hydra.css must be included before bootstrap-responsive to
|
||||
make the @media rule work. -->
|
||||
<link rel="stylesheet" href="/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/hydra.css") %]" type="text/css" />
|
||||
<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>
|
||||
.popover { max-width: 40%; }
|
||||
</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="/static/css/logfile.css" type="text/css" />
|
||||
<link rel="stylesheet" href="[% c.uri_for("/static/css/tree.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 %]
|
||||
|
||||
|
@ -68,24 +66,27 @@
|
|||
|
||||
<div class="container">
|
||||
|
||||
[% IF !hideHeader %]
|
||||
<div class="page-header">
|
||||
<h1><small>[% HTML.escape(title) %]</small></h1>
|
||||
</div>
|
||||
[% ELSE %]
|
||||
<br />
|
||||
[% END %]
|
||||
|
||||
[% IF flashMsg %]
|
||||
<br />
|
||||
<div class="alert alert-info">[% flashMsg %]</div>
|
||||
[% END %]
|
||||
|
||||
[% IF successMsg %]
|
||||
<br />
|
||||
<div class="alert alert-success">[% successMsg %]</div>
|
||||
[% END %]
|
||||
|
||||
[% 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 %]
|
||||
|
||||
[% content %]
|
||||
|
@ -93,7 +94,7 @@
|
|||
<footer class="navbar">
|
||||
<hr />
|
||||
<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 %]
|
||||
You are signed in as <tt>[% HTML.escape(c.user.username) %]</tt>.
|
||||
[% END %]
|
||||
|
|
|
@ -13,14 +13,14 @@ You are already signed in as <tt>[% HTML.escape(c.user.username) %]</tt>.
|
|||
<div class="control-group">
|
||||
<label class="control-label">User name</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="span3" name="username" value=""></input>
|
||||
<input type="text" class="span3" name="username" value=""/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="span3" name="password" value=""></input>
|
||||
<input type="password" class="span3" name="password" value=""/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<tr class="product">
|
||||
<td>
|
||||
<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.
|
||||
</a>
|
||||
[% WRAPPER makePopover title="Help" classes="btn-mini" %]
|
||||
|
@ -59,7 +59,7 @@
|
|||
<td>
|
||||
[% uri = c.uri_for('/build' build.id 'nix' 'pkg' "${build.nixname}-${build.system}.nixpkg") %]
|
||||
<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>
|
||||
</a>
|
||||
[% WRAPPER makePopover title="Help" classes="btn-mini" %]
|
||||
|
@ -87,7 +87,7 @@
|
|||
[% uri = c.uri_for('/build' build.id 'nix' 'closure' filename ) %]
|
||||
|
||||
<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>
|
||||
</a>
|
||||
|
||||
|
@ -127,17 +127,17 @@
|
|||
<a href="[% uri %]">
|
||||
[% SWITCH product.subtype %]
|
||||
[% 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" %]
|
||||
<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" %]
|
||||
<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" %]
|
||||
<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" %]
|
||||
<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" %]
|
||||
<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 %]
|
||||
File <tt>[% product.name %]</tt> of type <tt>[% product.subtype %]</tt>
|
||||
[% END %]
|
||||
|
@ -160,7 +160,7 @@
|
|||
<tr class="product">
|
||||
<td>
|
||||
<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 %]
|
||||
[% CASE "coverage" %]
|
||||
Code coverage analysis report
|
||||
|
@ -177,9 +177,9 @@
|
|||
<td>
|
||||
<a href="[% uri %]">
|
||||
[% 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 %]
|
||||
<img src="/static/images/document.png" alt="Document" />
|
||||
<img src="[% c.uri_for("/static/images/document.png") %]" alt="Document" />
|
||||
[% END %]
|
||||
[% SWITCH product.subtype %]
|
||||
[% CASE "readme" %]
|
||||
|
|
|
@ -2,16 +2,33 @@
|
|||
[% PROCESS common.tt %]
|
||||
|
||||
<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><a href="#tabs-configuration" data-toggle="tab">Configuration</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>
|
||||
[% END %]
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
<div id="tabs-project" class="tab-pane active">
|
||||
[% IF project.jobsets.size > 0 %]
|
||||
[% IF project.jobsets %]
|
||||
<p>This project has the following jobsets:</p>
|
||||
|
||||
<table class="table table-striped table-condensed clickable-rows">
|
||||
|
@ -30,18 +47,18 @@
|
|||
<tr>
|
||||
<td>
|
||||
[% 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 %]
|
||||
<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 %]
|
||||
<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 %]
|
||||
<img src="/static/images/error_16.png" alt="All Failed" />
|
||||
<img src="[% c.uri_for("/static/images/error_16.png") %]" alt="All Failed" />
|
||||
[% END %]
|
||||
</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>[% INCLUDE renderDateTime timestamp = j.lastcheckedtime %]</td>
|
||||
<td>[% IF j.lastcheckedtime; INCLUDE renderDateTime timestamp = j.lastcheckedtime; ELSE; "-"; END %]</td>
|
||||
[% IF j.get_column('nrtotal') > 0 %]
|
||||
[% successrate = ( j.get_column('nrsucceeded') / j.get_column('nrtotal') )*100 %]
|
||||
[% IF j.get_column('nrscheduled') > 0 %]
|
||||
|
@ -78,7 +95,6 @@
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<tr>
|
||||
<th>Display name:</th>
|
||||
|
@ -138,12 +154,6 @@
|
|||
|
||||
[% 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 id="tabs-views" class="tab-pane">
|
||||
|
@ -176,4 +186,18 @@
|
|||
|
||||
</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 %]
|
||||
|
|
|
@ -141,11 +141,14 @@ fi
|
|||
args+=(--arg '[% input.name %]' "{ outPath = $inputDir; rev = \"[% input.revision %]\"; }")
|
||||
|
||||
[% ELSIF input.type == "string" %]
|
||||
args+=(--arg '[% input.name %]' '"[% input.value %]"')
|
||||
args+=(--arg '[% input.name %]' '"[% input.value %]"') # FIXME: escape
|
||||
|
||||
[% ELSIF input.type == "boolean" %]
|
||||
args+=(--arg '[% input.name %]' '[% input.value %]')
|
||||
|
||||
[% ELSIF input.type == "nix" %]
|
||||
args+=(--arg '[% input.name %]' '[% input.value %]') # FIXME: escape
|
||||
|
||||
[% ELSE %]
|
||||
echo "$0: input ‘[% input.name %]’ has unsupported type ‘[% input.type %]’" >&2
|
||||
exit 1
|
||||
|
|
|
@ -91,3 +91,20 @@ div.news-item:not(:first-child) {
|
|||
td.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.override-link a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.actions {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.star {
|
||||
color: black;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.star:hover {
|
||||
cursor: pointer;
|
||||
}
|
52
src/root/static/css/rotated-th.css
Normal file
52
src/root/static/css/rotated-th.css
Normal 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*/
|
||||
}
|
BIN
src/root/static/images/forbidden_16.png
Normal file
BIN
src/root/static/images/forbidden_16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 672 B |
BIN
src/root/static/images/warning_16.png
Normal file
BIN
src/root/static/images/warning_16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 643 B |
|
@ -50,6 +50,17 @@ $(document).ready(function() {
|
|||
|
||||
$(".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
|
||||
navigates back to the previously selected tab on this
|
||||
page. */
|
||||
|
@ -58,10 +69,79 @@ $(document).ready(function() {
|
|||
var id = e.target.toString().match(pattern)[0];
|
||||
history.replaceState(null, "", id);
|
||||
});
|
||||
});
|
||||
|
||||
$(function() {
|
||||
if (window.location.hash) {
|
||||
$(".nav a[href='" + window.location.hash + "']").tab('show');
|
||||
/* Automatically set Bootstrap radio buttons from hidden form controls. */
|
||||
$('div[data-toggle="buttons-radio"] input[type="hidden"]').map(function(){
|
||||
$('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);
|
||||
};
|
||||
|
|
|
@ -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 %]
|
||||
<li class="dropdown">
|
||||
<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 %]
|
||||
</ul>
|
||||
</li>
|
||||
[% 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" %]
|
||||
[% INCLUDE menuItem
|
||||
|
@ -39,15 +37,7 @@
|
|||
<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('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" %]
|
||||
[% 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 %]
|
||||
|
||||
|
@ -64,40 +54,7 @@
|
|||
[% INCLUDE menuItem
|
||||
uri = c.uri_for(c.controller('Jobset').action_for('all'), [project.name, jobset.name])
|
||||
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" %]
|
||||
[% 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 %]
|
||||
|
||||
|
@ -111,54 +68,10 @@
|
|||
[% INCLUDE menuItem
|
||||
uri = c.uri_for(c.controller('Job').action_for('all'), [project.name, jobset.name, job.name])
|
||||
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" %]
|
||||
[% 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') %]
|
||||
[% WRAPPER makeSubMenu title="Admin" %]
|
||||
[% IF c.check_user_roles('admin') %]
|
||||
|
@ -182,18 +95,23 @@
|
|||
class = "" %]
|
||||
[% INCLUDE menuItem
|
||||
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?"
|
||||
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 %]
|
||||
|
||||
</ul>
|
||||
|
||||
<ul class="nav pull-right" id="top-menu">
|
||||
<ul class="nav pull-right">
|
||||
|
||||
<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>
|
||||
|
||||
[% IF c.user_exists %]
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
[% BREAK IF checked %]
|
||||
[% END %]
|
||||
[% IF checked %]
|
||||
SELECTED
|
||||
selected="selected"
|
||||
[% END %]
|
||||
>[% role %]</option>
|
||||
[% END %]
|
||||
|
@ -22,7 +22,7 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">User name</label>
|
||||
<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>
|
||||
[% END %]
|
||||
|
@ -30,7 +30,7 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">Full name</label>
|
||||
<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>
|
||||
|
||||
|
@ -38,14 +38,14 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">Password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="span3" name="password" value=""></input>
|
||||
<input type="password" class="span3" name="password" value=""/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">Confirm password</label>
|
||||
<div class="controls">
|
||||
<input type="password" class="span3" name="password2" value=""></input>
|
||||
<input type="password" class="span3" name="password2" value=""/>
|
||||
</div>
|
||||
</div>
|
||||
[% END %]
|
||||
|
@ -54,7 +54,7 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">Email</label>
|
||||
<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>
|
||||
-->
|
||||
|
@ -63,7 +63,7 @@
|
|||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -73,7 +73,7 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">Roles</label>
|
||||
<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="create-project" %]
|
||||
</select>
|
||||
|
@ -91,7 +91,7 @@
|
|||
<div class="control-group">
|
||||
<label class="control-label">Type the digits shown in the image above</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="span3" name="captcha" value=""></input>
|
||||
<input type="text" class="span3" name="captcha" value=""/>
|
||||
</div>
|
||||
</div>
|
||||
[% END %]
|
||||
|
@ -137,7 +137,8 @@
|
|||
</script>
|
||||
[% END %]
|
||||
</div>
|
||||
</p>
|
||||
|
||||
</fieldset>
|
||||
|
||||
</form>
|
||||
|
||||
|
|
|
@ -27,11 +27,11 @@
|
|||
<tr>
|
||||
<td>
|
||||
[% 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 %]
|
||||
<img src="/static/images/error_16.png" />
|
||||
<img src="[% c.uri_for("/static/images/error_16.png") %]" />
|
||||
[% ELSIF result.status == 2 %]
|
||||
<img src="/static/images/help_16.png" />
|
||||
<img src="[% c.uri_for("/static/images/help_16.png") %]" />
|
||||
[% END %]
|
||||
</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 %]
|
||||
<a href="[% c.uri_for('/build' j.build.id) %]">
|
||||
[% 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 %]
|
||||
<img src="/static/images/error_16.png" />
|
||||
<img src="[% c.uri_for("/static/images/error_16.png") %]" />
|
||||
[% END %]
|
||||
</a>
|
||||
[% END %]
|
||||
|
|
|
@ -10,6 +10,7 @@ distributable_scripts = \
|
|||
hydra-queue-runner \
|
||||
hydra-server \
|
||||
hydra-update-gc-roots \
|
||||
hydra-s3-backup-collect-garbage \
|
||||
nix-prefetch-git \
|
||||
nix-prefetch-bzr \
|
||||
nix-prefetch-hg
|
||||
|
|
|
@ -8,8 +8,10 @@ use Nix::Store;
|
|||
use Hydra::Plugin;
|
||||
use Hydra::Schema;
|
||||
use Hydra::Helper::Nix;
|
||||
use Hydra::Helper::PluginHooks;
|
||||
use Hydra::Model::DB;
|
||||
use Hydra::Helper::AddBuilds;
|
||||
use Set::Scalar;
|
||||
|
||||
STDOUT->autoflush();
|
||||
|
||||
|
@ -40,16 +42,18 @@ sub failDependents {
|
|||
my ($drvPath, $status, $errorMsg, $dependents) = @_;
|
||||
|
||||
# Get the referrer closure of $drvPath.
|
||||
my @dependentDrvs = computeFSClosure(1, 0, $drvPath);
|
||||
my $dependentDrvs = Set::Scalar->new(computeFSClosure(1, 0, $drvPath));
|
||||
|
||||
my $time = time();
|
||||
|
||||
txn_do($db, sub {
|
||||
|
||||
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) {
|
||||
next unless $dependentDrvs->has($d->drvpath);
|
||||
print STDERR "failing dependent build ", $d->id, " of ", $d->project->name, ":", $d->jobset->name, ":", $d->job->name, "\n";
|
||||
$d->update(
|
||||
{ finished => 1
|
||||
|
@ -67,8 +71,8 @@ sub failDependents {
|
|||
, drvpath => $drvPath
|
||||
, busy => 0
|
||||
, status => $status
|
||||
, starttime => time
|
||||
, stoptime => time
|
||||
, starttime => $time
|
||||
, stoptime => $time
|
||||
, errormsg => $errorMsg
|
||||
});
|
||||
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 {
|
||||
my ($build) = @_;
|
||||
|
||||
|
@ -132,7 +123,9 @@ sub doBuild {
|
|||
# associated log files, etc.
|
||||
my $cmd = "nix-store --realise $drvPath " .
|
||||
"--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 " .
|
||||
"--add-root " . gcRootFor($outputs{out} // $outputs{(sort keys %outputs)[0]}) . " 2>&1";
|
||||
|
||||
|
@ -149,6 +142,22 @@ sub doBuild {
|
|||
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+)$/) {
|
||||
my $drvPathStep = $1;
|
||||
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'}) {
|
||||
my $build = $db->resultset('Builds')->find($buildId);
|
||||
notify($build, []);
|
||||
notifyBuildFinished(\@plugins, $build, []);
|
||||
exit 0;
|
||||
}
|
||||
|
||||
|
@ -345,8 +354,8 @@ txn_do($db, sub {
|
|||
die "build $buildId is already being built";
|
||||
}
|
||||
$build->update({busy => 1, locker => $$});
|
||||
$build->buildsteps->search({busy => 1})->delete_all;
|
||||
$build->buildproducts->delete_all;
|
||||
$build->buildsteps->search({busy => 1})->delete;
|
||||
$build->buildproducts->delete;
|
||||
});
|
||||
|
||||
die unless $build;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use strict;
|
||||
use feature 'switch';
|
||||
use utf8;
|
||||
use Hydra::Schema;
|
||||
use Hydra::Plugin;
|
||||
use Hydra::Helper::Nix;
|
||||
|
@ -33,7 +34,7 @@ sub fetchInputs {
|
|||
foreach my $input ($jobset->jobsetinputs->all) {
|
||||
foreach my $alt ($input->jobsetinputalts->all) {
|
||||
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 {
|
||||
my ($jobset, $errorMsg) = @_;
|
||||
my $prevError = $jobset->errormsg;
|
||||
|
||||
eval {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -65,7 +70,7 @@ sub sendJobsetErrorNotification() {
|
|||
|
||||
my $body = "Hi,\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"
|
||||
. "\n"
|
||||
. "$errorMsg"
|
||||
|
@ -110,7 +115,17 @@ sub checkJobsetWrapped {
|
|||
|
||||
# Fetch all values for all inputs.
|
||||
my $checkoutStart = time;
|
||||
eval {
|
||||
fetchInputs($project, $jobset, $inputInfo);
|
||||
};
|
||||
if ($@) {
|
||||
my $msg = $@;
|
||||
print STDERR $msg;
|
||||
txn_do($db, sub {
|
||||
$jobset->update({ lastcheckedtime => time, fetcherrormsg => $msg });
|
||||
});
|
||||
return;
|
||||
}
|
||||
my $checkoutStop = time;
|
||||
|
||||
# Hash the arguments to hydra-eval-jobs and check the
|
||||
|
@ -122,14 +137,14 @@ sub checkJobsetWrapped {
|
|||
if (defined $prevEval && $prevEval->hash eq $argsHash) {
|
||||
print STDERR " jobset is unchanged, skipping\n";
|
||||
txn_do($db, sub {
|
||||
$jobset->update({lastcheckedtime => time});
|
||||
$jobset->update({ lastcheckedtime => time, fetcherrormsg => undef });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
# Evaluate the job expression.
|
||||
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 $jobOutPathMap = {};
|
||||
|
@ -144,11 +159,11 @@ sub checkJobsetWrapped {
|
|||
$jobset->builds->search({iscurrent => 1})->update({iscurrent => 0});
|
||||
|
||||
# Schedule each successfully evaluated job.
|
||||
my %buildIds;
|
||||
my %buildMap;
|
||||
foreach my $job (permute @{$jobs->{job}}) {
|
||||
next if $job->{jobName} eq "";
|
||||
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
|
||||
|
@ -161,23 +176,51 @@ sub checkJobsetWrapped {
|
|||
$_->update({ errormsg => $failedJobNames{$_->name} ? join '\n', @{$failedJobNames{$_->name}} : undef })
|
||||
foreach $jobset->jobs->all;
|
||||
|
||||
my $hasNewBuilds = 0;
|
||||
while (my ($id, $new) = each %buildIds) {
|
||||
$hasNewBuilds = 1 if $new;
|
||||
}
|
||||
# Have any builds been added or removed since last time?
|
||||
my $jobsetChanged =
|
||||
(scalar(grep { $_->{new} } values(%buildMap)) > 0)
|
||||
|| (defined $prevEval && $prevEval->jobsetevalmembers->count != scalar(keys %buildMap));
|
||||
|
||||
my $ev = $jobset->jobsetevals->create(
|
||||
{ hash => $argsHash
|
||||
, timestamp => time
|
||||
, checkouttime => abs($checkoutStop - $checkoutStart)
|
||||
, evaltime => abs($evalStop - $evalStart)
|
||||
, hasnewbuilds => $hasNewBuilds
|
||||
, nrbuilds => $hasNewBuilds ? scalar(keys %buildIds) : undef
|
||||
, hasnewbuilds => $jobsetChanged ? 1 : 0
|
||||
, nrbuilds => $jobsetChanged ? scalar(keys %buildMap) : undef
|
||||
});
|
||||
|
||||
if ($hasNewBuilds) {
|
||||
while (my ($id, $new) = each %buildIds) {
|
||||
$ev->jobsetevalmembers->create({ build => $id, isnew => $new });
|
||||
if ($jobsetChanged) {
|
||||
# Create JobsetEvalMembers mappings.
|
||||
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}) {
|
||||
|
@ -203,23 +246,16 @@ sub checkJobsetWrapped {
|
|||
print STDERR " created cached eval ", $ev->id, "\n";
|
||||
$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.
|
||||
my $msg = "";
|
||||
foreach my $error (@{$jobs->{error}}) {
|
||||
my $bindings = "";
|
||||
foreach my $arg (@{$error->{arg}}) {
|
||||
my $input = $inputInfo->{$arg->{name}}->[$arg->{altnr}] or die "invalid input";
|
||||
$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";
|
||||
$msg .=
|
||||
($error->{location} ne "" ? "in job ‘$error->{location}’" : "at top-level") .
|
||||
":\n" . $error->{msg} . "\n\n";
|
||||
}
|
||||
setJobsetError($jobset, $msg);
|
||||
}
|
||||
|
@ -241,7 +277,7 @@ sub checkJobset {
|
|||
|
||||
if ($@) {
|
||||
my $msg = $@;
|
||||
print STDERR "error evaluating jobset ", $jobset->name, ": $msg";
|
||||
print STDERR $msg;
|
||||
txn_do($db, sub {
|
||||
$jobset->update({lastcheckedtime => time});
|
||||
setJobsetError($jobset, $msg);
|
||||
|
@ -272,7 +308,7 @@ sub checkSomeJobset {
|
|||
# longest time (but don't check more often than the jobset's
|
||||
# minimal check interval).
|
||||
($jobset) = $db->resultset('Jobsets')->search(
|
||||
{ 'project.enabled' => 1, 'me.enabled' => 1,
|
||||
{ 'project.enabled' => 1, 'me.enabled' => { '!=' => 0 },
|
||||
, 'checkinterval' => { '!=', 0 }
|
||||
, -or => [ 'lastcheckedtime' => undef, 'lastcheckedtime' => { '<', \ (time() . " - me.checkinterval") } ] },
|
||||
{ join => 'project', order_by => [ 'lastcheckedtime nulls first' ], rows => 1 })
|
||||
|
@ -280,13 +316,10 @@ sub checkSomeJobset {
|
|||
|
||||
return 0 unless defined $jobset;
|
||||
|
||||
checkJobset($jobset);
|
||||
|
||||
return 1;
|
||||
return system($0, $jobset->project->name, $jobset->name) == 0;
|
||||
}
|
||||
|
||||
|
||||
# For testing: evaluate a single jobset, then exit.
|
||||
if (scalar @ARGV == 2) {
|
||||
my $projectName = $ARGV[0];
|
||||
my $jobsetName = $ARGV[1];
|
||||
|
|
|
@ -51,12 +51,12 @@ for (my $n = $schemaVersion; $n < $maxSchemaVersion; $n++) {
|
|||
my @statements = $sql_splitter->split($schema);
|
||||
eval {
|
||||
$dbh->begin_work;
|
||||
sub run {
|
||||
sub run_ {
|
||||
my ($stm) = @_;
|
||||
print STDERR "executing SQL statement: $stm\n";
|
||||
$dbh->do($_);
|
||||
}
|
||||
run($_) foreach @statements;
|
||||
run_($_) foreach @statements;
|
||||
$db->resultset('SchemaVersion')->update({version => $m});
|
||||
$dbh->commit;
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ use Hydra::Helper::Nix;
|
|||
use Hydra::Model::DB;
|
||||
use IO::Handle;
|
||||
use Nix::Store;
|
||||
use Set::Scalar;
|
||||
|
||||
chdir Hydra::Model::DB::getHydraPath or die;
|
||||
my $db = Hydra::Model::DB->new();
|
||||
|
@ -36,7 +37,7 @@ sub unlockDeadBuilds {
|
|||
}
|
||||
if ($unlock) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
@ -52,14 +53,25 @@ sub findBuildDependencyInQueue {
|
|||
my @deps = grep { /\.drv$/ && $_ ne $build->drvpath } computeFSClosure(0, 0, $build->drvpath);
|
||||
return unless scalar @deps > 0;
|
||||
foreach my $d (@deps) {
|
||||
my $b = $buildsByDrv->{$d};
|
||||
next unless defined $b;
|
||||
return $db->resultset('Builds')->find($b);
|
||||
my $bs = $buildsByDrv->{$d};
|
||||
next unless defined $bs;
|
||||
return $db->resultset('Builds')->find((@$bs)[0]);
|
||||
}
|
||||
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 {
|
||||
# print "looking for runnable builds...\n";
|
||||
|
||||
|
@ -70,27 +82,34 @@ sub checkBuilds {
|
|||
my %maxConcurrent;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
txn_do($db, sub {
|
||||
|
||||
# Cache scheduled by derivation path to speed up
|
||||
# Cache scheduled builds by derivation path to speed up
|
||||
# findBuildDependencyInQueue.
|
||||
my $buildsByDrv = {};
|
||||
$buildsByDrv->{$_->drvpath} = $_->id
|
||||
foreach $db->resultset('Builds')->search({ finished => 0, enabled => 1 }, { join => ['project'] });
|
||||
push @{$buildsByDrv->{$_->drvpath}}, $_->id
|
||||
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.
|
||||
my @systemTypes = $db->resultset('Builds')->search(
|
||||
{ finished => 0, busy => 0, enabled => 1 },
|
||||
{ finished => 0, busy => 0 },
|
||||
{ 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
|
||||
# concurrent build for that system type. Choose the highest
|
||||
# priority builds first, then the oldest builds.
|
||||
# concurrent build for that system type.
|
||||
foreach my $system (@systemTypes) {
|
||||
# How many builds are already currently executing for this
|
||||
# system type?
|
||||
|
@ -101,15 +120,67 @@ sub checkBuilds {
|
|||
my $max = defined $systemTypeInfo ? $systemTypeInfo->maxconcurrent : $maxConcurrent{$system->system} // 2;
|
||||
|
||||
my $extraAllowed = $max - $nrActive;
|
||||
$extraAllowed = 0 if $extraAllowed < 0;
|
||||
next if $extraAllowed <= 0;
|
||||
|
||||
# Select the highest-priority builds to start.
|
||||
my @builds = $extraAllowed == 0 ? () : $db->resultset('Builds')->search(
|
||||
{ finished => 0, busy => 0, system => $system->system, enabled => 1 },
|
||||
{ join => ['project'], order_by => ["priority DESC", "id"] });
|
||||
print STDERR "starting at most $extraAllowed builds for system ${\$system->system}\n";
|
||||
|
||||
my $timeSpentPerJobset;
|
||||
|
||||
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) {
|
||||
next if $blockedBuilds->has($build->id);
|
||||
|
||||
# Find a dependency of $build that has no queued
|
||||
# dependencies itself. This isn't strictly necessary,
|
||||
# but it ensures that Nix builds are done as part of
|
||||
|
@ -120,6 +191,9 @@ sub checkBuilds {
|
|||
}
|
||||
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;
|
||||
mkdir(dirname $logfile);
|
||||
unlink($logfile);
|
||||
|
@ -127,25 +201,30 @@ sub checkBuilds {
|
|||
{ busy => 1
|
||||
, locker => $$
|
||||
, logfile => $logfile
|
||||
, starttime => time()
|
||||
});
|
||||
push @buildsStarted, $build;
|
||||
|
||||
last if ++$started >= $extraAllowed;
|
||||
$timeSpentPerJobset->{$jobset->get_column('project')}->{$jobset->name} += $costPerBuild;
|
||||
|
||||
blockBuilds($buildsByDrv, $blockedBuilds, $build);
|
||||
|
||||
next j;
|
||||
}
|
||||
}
|
||||
|
||||
if ($started > 0) {
|
||||
print STDERR "system type `", $system->system,
|
||||
"': $nrActive active, $max allowed, started $started builds\n";
|
||||
last; # nothing found, give up on this system type
|
||||
}
|
||||
}
|
||||
|
||||
$lastTime = time();
|
||||
|
||||
$_->update({ starttime => time() }) foreach @buildsStarted;
|
||||
});
|
||||
|
||||
# Actually start the builds we just selected. We need to do this
|
||||
# outside the transaction in case it aborts or something.
|
||||
foreach my $build (@buildsStarted) {
|
||||
my $id = $build->id;
|
||||
print "starting build $id (", $build->project->name, ":", $build->jobset->name, ':', $build->job->name, ") on ", $build->system, "\n";
|
||||
eval {
|
||||
my $logfile = $build->logfile;
|
||||
my $child = fork();
|
||||
|
@ -164,9 +243,7 @@ sub checkBuilds {
|
|||
if ($@) {
|
||||
warn $@;
|
||||
txn_do($db, sub {
|
||||
$build->busy(0);
|
||||
$build->locker($$);
|
||||
$build->update;
|
||||
$build->update({ busy => 0, locker => $$ });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -187,8 +264,6 @@ while (1) {
|
|||
unlockDeadBuilds;
|
||||
|
||||
checkBuilds;
|
||||
|
||||
$lastTime = time();
|
||||
};
|
||||
warn $@ if $@;
|
||||
|
||||
|
|
58
src/script/hydra-s3-backup-collect-garbage
Executable file
58
src/script/hydra-s3-backup-collect-garbage
Executable 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;
|
|
@ -56,6 +56,11 @@ my @roots = readdir 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.
|
||||
print STDERR "*** looking for release members\n";
|
||||
keepBuild $_ foreach $db->resultset('Builds')->search_literal(
|
||||
|
@ -84,50 +89,33 @@ foreach my $project ($db->resultset('Projects')->search({}, { order_by => ["name
|
|||
next;
|
||||
}
|
||||
|
||||
if ($keepnr <= 0 ) {
|
||||
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 ",
|
||||
print STDERR "*** looking for all builds in the unfinished and $keepnr most recent finished evaluations of jobset ",
|
||||
$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(
|
||||
{ 'me.id' => { 'in' => \
|
||||
[ "select b2.id from Builds b2 join " .
|
||||
" (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 ] ] }
|
||||
{ finished => 1, buildStatus => { -in => [0, 6] }
|
||||
, id => { -in => $db->resultset('JobsetEvalMembers')->search({ eval => { -in => [@evals] } }, { select => "build" })->as_query }
|
||||
},
|
||||
{ order_by => ["job", "system", "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}};
|
||||
}
|
||||
{ order_by => ["job", "id"], columns => [ @columns ] });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 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.
|
||||
print STDERR "*** removing unneeded GC roots\n";
|
||||
|
||||
|
|
|
@ -55,12 +55,14 @@ create table Jobsets (
|
|||
errorTime integer, -- timestamp associated with errorMsg
|
||||
lastCheckedTime integer, -- last time the evaluator looked at this jobset
|
||||
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,
|
||||
hidden integer not null default 0,
|
||||
emailOverride text not null,
|
||||
keepnr integer not null default 3,
|
||||
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),
|
||||
foreign key (project) references Projects(name) on delete cascade on update cascade
|
||||
#ifdef SQLITE
|
||||
|
@ -74,7 +76,8 @@ create table JobsetInputs (
|
|||
project text not null,
|
||||
jobset 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),
|
||||
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,
|
||||
|
||||
-- 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.
|
||||
nixExprInput text,
|
||||
nixExprPath text,
|
||||
|
@ -255,6 +258,7 @@ create table BuildInputs (
|
|||
uri text,
|
||||
revision text,
|
||||
value text,
|
||||
emailResponsible integer not null default 0,
|
||||
dependency integer, -- build ID of the input, for type == 'build'
|
||||
|
||||
path text,
|
||||
|
@ -322,6 +326,15 @@ create table CachedGitInputs (
|
|||
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 (
|
||||
uri 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.
|
||||
|
||||
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 IndexBuildsOnProject on Builds(project);
|
||||
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 IndexBuildsOnJobSystemCurrent on Builds(project, jobset, job, system, isCurrent);
|
||||
create index IndexBuildsOnDrvPath on Builds(drvPath);
|
||||
|
|
23
src/sql/upgrade-17.sql
Normal file
23
src/sql/upgrade-17.sql
Normal 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
1
src/sql/upgrade-18.sql
Normal file
|
@ -0,0 +1 @@
|
|||
create index IndexBuildsOnFinishedStopTime on Builds(finished, stoptime DESC);
|
5
src/sql/upgrade-19.sql
Normal file
5
src/sql/upgrade-19.sql
Normal 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
8
src/sql/upgrade-20.sql
Normal 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
Loading…
Reference in a new issue