You've already forked tribes-supertest
cc9bec550b
Rename the repo Guix development manifest to manifest.scm and update the wrapper, direnv integration, and dev-branch helper to use local Guix by default with an optional --build-host.
670 lines
22 KiB
Perl
Executable File
670 lines
22 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Cwd qw(abs_path getcwd);
|
|
use File::Basename qw(dirname);
|
|
use File::Path qw(make_path);
|
|
use File::Spec;
|
|
use Getopt::Long qw(GetOptionsFromArray);
|
|
use IO::Select;
|
|
use IPC::Open3 qw(open3);
|
|
use JSON::PP qw(decode_json encode_json);
|
|
use POSIX qw(strftime);
|
|
use Symbol qw(gensym);
|
|
use Time::HiRes qw(time);
|
|
|
|
my $DEV_BRANCH = 'supertest-dev';
|
|
my $CHANNEL_URL = 'https://git.teralink.net/tribes/guix-tribes.git';
|
|
my $INTRO_COMMIT = '7c4f9d3b3477945ca75d22baa237c44895f2e454';
|
|
my $DEV_FINGERPRINT = 'F29B A6DA 96E5 EC29 FDDE D994 8F4F 75B3 B19D 4784';
|
|
my $DEV_FINGERPRINT_COMPACT = 'F29BA6DA96E5EC29FDDED9948F4F75B3B19D4784';
|
|
my $SIGNER_LABEL = 'tribes-supertest-dev';
|
|
my $DEV_KEY_FILE = 'supertest-dev-B19D4784.key';
|
|
|
|
my %COMMANDS = map { $_ => 1 } qw(prepare reset env run ssh rpc help);
|
|
|
|
sub usage {
|
|
print <<'EOF';
|
|
Usage:
|
|
scripts/test-dev-branch [options] <scenario>
|
|
scripts/test-dev-branch prepare [options]
|
|
scripts/test-dev-branch reset [options]
|
|
scripts/test-dev-branch env [options]
|
|
scripts/test-dev-branch run [options] <scenario>
|
|
scripts/test-dev-branch ssh [options] <node> [-- <command...>]
|
|
scripts/test-dev-branch rpc [options] <node> -- <elixir-expression>
|
|
|
|
Options:
|
|
--plugin NAME Update and test plugin NAME (default: no plugin override)
|
|
--plugin-repo PATH Plugin checkout to pin (default: ../tribes-plugin-NAME)
|
|
--keep-nodes Keep nodes after the scenario run
|
|
--cert-mode MODE self-signed (default) or acme
|
|
--run-id ID Override generated SUPERTEST_RUN_ID
|
|
--guix-repo PATH guix-tribes checkout (default: ../guix-tribes)
|
|
--tribes-repo PATH tribes checkout (default: ../tribes)
|
|
--legion-repo PATH legion_kk checkout (default: ../legion_kk)
|
|
--build-host HOST optional remote Guix build host for pin helpers (default: local Guix)
|
|
--dry-run Print commands that would run
|
|
-h, --help Show this help
|
|
|
|
The default form prepares the hard-coded supertest-dev guix-tribes branch and
|
|
then runs the requested scenario in self-signed mode.
|
|
EOF
|
|
}
|
|
|
|
sub fail {
|
|
die "@_\n";
|
|
}
|
|
|
|
sub shell_quote {
|
|
my ($value) = @_;
|
|
$value =~ s/'/'\\''/g;
|
|
return "'$value'";
|
|
}
|
|
|
|
sub display_cmd {
|
|
return join(' ', map { shell_quote($_) } @_);
|
|
}
|
|
|
|
sub compact_fingerprint {
|
|
my ($value) = @_;
|
|
$value =~ s/\s+//g;
|
|
return $value;
|
|
}
|
|
|
|
sub run_checked {
|
|
my ($ctx, @cmd) = @_;
|
|
print "+ ", display_cmd(@cmd), "\n" if $ctx->{verbose} || $ctx->{dry_run};
|
|
return if $ctx->{dry_run};
|
|
system(@cmd) == 0 or fail("Command failed: " . display_cmd(@cmd));
|
|
}
|
|
|
|
sub capture {
|
|
my (@cmd) = @_;
|
|
my $err = gensym;
|
|
my $pid = open3(my $in, my $out, $err, @cmd);
|
|
close $in;
|
|
|
|
my $output = '';
|
|
my $select = IO::Select->new($out, $err);
|
|
while (my @ready = $select->can_read) {
|
|
for my $fh (@ready) {
|
|
my $chunk = '';
|
|
my $bytes = sysread($fh, $chunk, 8192);
|
|
if (defined $bytes && $bytes > 0) {
|
|
$output .= $chunk;
|
|
next;
|
|
}
|
|
|
|
$select->remove($fh);
|
|
close $fh;
|
|
}
|
|
}
|
|
|
|
waitpid($pid, 0);
|
|
my $status = $? >> 8;
|
|
return ($status, $output);
|
|
}
|
|
|
|
sub capture_checked {
|
|
my (@cmd) = @_;
|
|
my ($status, $output) = capture(@cmd);
|
|
$status == 0 or fail("Command failed: " . display_cmd(@cmd) . "\n$output");
|
|
chomp $output;
|
|
return $output;
|
|
}
|
|
|
|
sub read_file {
|
|
my ($path) = @_;
|
|
open my $fh, '<', $path or fail("Failed to read $path: $!");
|
|
local $/;
|
|
return <$fh>;
|
|
}
|
|
|
|
sub write_file {
|
|
my ($path, $content) = @_;
|
|
open my $fh, '>', $path or fail("Failed to write $path: $!");
|
|
print {$fh} $content or fail("Failed to write $path: $!");
|
|
close $fh or fail("Failed to close $path: $!");
|
|
}
|
|
|
|
sub write_json_file {
|
|
my ($path, $data) = @_;
|
|
my $json = JSON::PP->new->canonical->pretty->encode($data);
|
|
write_file($path, $json);
|
|
}
|
|
|
|
sub repo_root {
|
|
my $script_dir = abs_path(dirname($0));
|
|
return abs_path(File::Spec->catdir($script_dir, '..'));
|
|
}
|
|
|
|
sub abs_default {
|
|
my ($base, $path) = @_;
|
|
return abs_path(File::Spec->rel2abs($path, $base));
|
|
}
|
|
|
|
sub parse_args {
|
|
my @argv = @_;
|
|
my $root = repo_root();
|
|
my %opts = (
|
|
cert_mode => 'self-signed',
|
|
|
|
dry_run => 0,
|
|
keep_nodes => 0,
|
|
guix_repo => abs_default($root, '../guix-tribes'),
|
|
tribes_repo => abs_default($root, '../tribes'),
|
|
legion_repo => abs_default($root, '../legion_kk'),
|
|
);
|
|
|
|
GetOptionsFromArray(
|
|
\@argv,
|
|
'plugin=s' => \$opts{plugin},
|
|
'plugin-repo=s' => \$opts{plugin_repo},
|
|
'keep-nodes' => \$opts{keep_nodes},
|
|
'cert-mode=s' => \$opts{cert_mode},
|
|
'run-id=s' => \$opts{run_id},
|
|
'guix-repo=s' => \$opts{guix_repo},
|
|
'tribes-repo=s' => \$opts{tribes_repo},
|
|
'legion-repo=s' => \$opts{legion_repo},
|
|
'build-host=s' => \$opts{build_host},
|
|
'dry-run' => \$opts{dry_run},
|
|
'h|help' => \$opts{help},
|
|
) or do {
|
|
usage();
|
|
exit 1;
|
|
};
|
|
|
|
if ($opts{help}) {
|
|
usage();
|
|
exit 0;
|
|
}
|
|
|
|
$opts{guix_repo} = abs_default($root, $opts{guix_repo});
|
|
$opts{tribes_repo} = abs_default($root, $opts{tribes_repo});
|
|
$opts{legion_repo} = abs_default($root, $opts{legion_repo});
|
|
$opts{plugin_repo} = abs_default($root, $opts{plugin_repo}) if defined $opts{plugin_repo};
|
|
$opts{root} = $root;
|
|
|
|
my $command = shift(@argv);
|
|
if (!defined $command) {
|
|
usage();
|
|
exit 1;
|
|
}
|
|
|
|
if (!$COMMANDS{$command}) {
|
|
unshift @argv, $command;
|
|
$command = 'run';
|
|
}
|
|
|
|
validate_cert_mode($opts{cert_mode});
|
|
|
|
return ($command, \%opts, \@argv);
|
|
}
|
|
|
|
sub validate_cert_mode {
|
|
my ($mode) = @_;
|
|
return if $mode eq 'self-signed' || $mode eq 'acme';
|
|
fail("Unsupported --cert-mode $mode; expected self-signed or acme.");
|
|
}
|
|
|
|
sub require_repo {
|
|
my ($path, $label) = @_;
|
|
my $git = File::Spec->catfile($path, '.git');
|
|
(-d $git || -f $git) or fail("$label repo not found: $path");
|
|
}
|
|
|
|
sub require_clean_repo {
|
|
my ($repo, $label) = @_;
|
|
my $status = capture_checked('git', '-C', $repo, 'status', '--porcelain');
|
|
$status eq '' or fail("$label repo has uncommitted changes:\n$status");
|
|
}
|
|
|
|
sub require_source_commit_pushed {
|
|
my ($ctx, $repo, $label, $commit) = @_;
|
|
run_checked($ctx, 'git', '-C', $repo, 'fetch', 'origin');
|
|
my $branches = capture_checked('git', '-C', $repo, 'branch', '-r', '--contains', $commit);
|
|
$branches =~ /\borigin\//
|
|
or fail("$label commit $commit is not reachable from any origin branch. Push the source repo first.");
|
|
}
|
|
|
|
sub current_commit {
|
|
my ($repo, $rev) = @_;
|
|
return capture_checked('git', '-C', $repo, 'rev-parse', "$rev^{commit}");
|
|
}
|
|
|
|
sub current_branch {
|
|
my ($repo) = @_;
|
|
return capture_checked('git', '-C', $repo, 'branch', '--show-current');
|
|
}
|
|
|
|
sub ensure_dev_signing_key {
|
|
my ($ctx) = @_;
|
|
my $key = capture_checked('git', '-C', $ctx->{guix_repo}, 'config', '--get', 'user.signingkey');
|
|
$key eq $DEV_FINGERPRINT_COMPACT
|
|
or fail("guix-tribes user.signingkey is $key, expected $DEV_FINGERPRINT_COMPACT.");
|
|
}
|
|
|
|
sub branch_contains {
|
|
my ($repo, $ancestor, $descendant) = @_;
|
|
my ($status, undef) = capture('git', '-C', $repo, 'merge-base', '--is-ancestor', $ancestor, $descendant);
|
|
return $status == 0;
|
|
}
|
|
|
|
sub ensure_dev_branch_ready {
|
|
my ($ctx) = @_;
|
|
my $repo = $ctx->{guix_repo};
|
|
my $branch = current_branch($repo);
|
|
my $local_ready = branch_contains($repo, $INTRO_COMMIT, $DEV_BRANCH);
|
|
my $remote_ready = branch_contains($repo, $INTRO_COMMIT, "origin/$DEV_BRANCH");
|
|
my $start_point =
|
|
$local_ready ? $DEV_BRANCH :
|
|
$remote_ready ? "origin/$DEV_BRANCH" :
|
|
undef;
|
|
|
|
defined $start_point
|
|
or fail("$DEV_BRANCH does not descend from intro commit $INTRO_COMMIT locally or on origin. Run reset first.");
|
|
|
|
if ($branch ne $DEV_BRANCH || !$local_ready) {
|
|
if ($ctx->{dry_run}) {
|
|
run_checked($ctx, 'git', '-C', $repo, 'checkout', '-B', $DEV_BRANCH, $start_point);
|
|
return $start_point;
|
|
}
|
|
run_checked($ctx, 'git', '-C', $repo, 'checkout', '-B', $DEV_BRANCH, $start_point);
|
|
}
|
|
|
|
branch_contains($repo, $INTRO_COMMIT, 'HEAD')
|
|
or fail("$DEV_BRANCH does not descend from intro commit $INTRO_COMMIT. Run reset first.");
|
|
|
|
my $auth = read_file(File::Spec->catfile($repo, '.guix-authorizations'));
|
|
compact_fingerprint($auth) =~ /\Q$DEV_FINGERPRINT_COMPACT\E/
|
|
or fail("$DEV_BRANCH does not authorize the supertest dev signing key. Run reset first.");
|
|
|
|
return $start_point;
|
|
}
|
|
|
|
sub authenticate_dev_branch {
|
|
my ($ctx) = @_;
|
|
my $end = current_commit($ctx->{guix_repo}, 'HEAD');
|
|
run_checked(
|
|
$ctx,
|
|
'guix', 'git', 'authenticate',
|
|
$INTRO_COMMIT,
|
|
$DEV_FINGERPRINT_COMPACT,
|
|
'--repository=' . $ctx->{guix_repo},
|
|
'--end=' . $end,
|
|
);
|
|
}
|
|
|
|
sub require_dev_branch_synced_to_master {
|
|
my ($ctx) = @_;
|
|
my $repo = $ctx->{guix_repo};
|
|
my $sync_file = File::Spec->catfile($repo, '.supertest-dev-sync.json');
|
|
-f $sync_file or fail("$DEV_BRANCH has no .supertest-dev-sync.json. Run scripts/test-dev-branch reset first.");
|
|
|
|
my $sync = decode_json(read_file($sync_file));
|
|
my $source_commit = $sync->{source_commit} // '';
|
|
my $master_commit = current_commit($repo, 'origin/master');
|
|
$source_commit eq $master_commit
|
|
or fail("$DEV_BRANCH is synced to master $source_commit, but current origin/master is $master_commit. Run scripts/test-dev-branch reset first.");
|
|
}
|
|
|
|
sub commit_if_changed {
|
|
my ($ctx, $message) = @_;
|
|
my $repo = $ctx->{guix_repo};
|
|
if ($ctx->{dry_run}) {
|
|
print "Would commit guix-tribes changes if pin helpers changed files.\n";
|
|
run_checked($ctx, 'git', '-C', $repo, 'add', 'tribes/packages/source.scm', 'tribes/plugins');
|
|
run_checked($ctx, 'git', '-C', $repo, 'commit', '-S' . $DEV_FINGERPRINT_COMPACT, '-m', $message);
|
|
return 1;
|
|
}
|
|
|
|
my $status = capture_checked('git', '-C', $repo, 'status', '--porcelain');
|
|
if ($status eq '') {
|
|
print "No guix-tribes changes to commit.\n";
|
|
return 0;
|
|
}
|
|
|
|
run_checked($ctx, 'git', '-C', $repo, 'add', 'tribes/packages/source.scm', 'tribes/plugins');
|
|
run_checked($ctx, 'git', '-C', $repo, 'commit', '-S' . $DEV_FINGERPRINT_COMPACT, '-m', $message);
|
|
return 1;
|
|
}
|
|
|
|
sub ensure_plugin_name {
|
|
my ($name) = @_;
|
|
defined $name && $name =~ /\A[a-z0-9][a-z0-9_-]*\z/
|
|
or fail("Invalid plugin name: " . (defined $name ? $name : ''));
|
|
}
|
|
|
|
sub plugin_repo_for {
|
|
my ($ctx, $plugin) = @_;
|
|
return $ctx->{plugin_repo} if defined $ctx->{plugin_repo};
|
|
return abs_default($ctx->{root}, "../tribes-plugin-$plugin");
|
|
}
|
|
|
|
sub prepare {
|
|
my ($ctx) = @_;
|
|
require_repo($ctx->{guix_repo}, 'guix-tribes');
|
|
require_repo($ctx->{tribes_repo}, 'tribes');
|
|
require_clean_repo($ctx->{guix_repo}, 'guix-tribes');
|
|
require_clean_repo($ctx->{tribes_repo}, 'tribes');
|
|
ensure_dev_signing_key($ctx);
|
|
|
|
my $tribes_commit = current_commit($ctx->{tribes_repo}, 'HEAD');
|
|
require_source_commit_pushed($ctx, $ctx->{tribes_repo}, 'tribes', $tribes_commit);
|
|
|
|
my ($plugin, $plugin_repo, $plugin_commit);
|
|
if (defined $ctx->{plugin}) {
|
|
$plugin = $ctx->{plugin};
|
|
ensure_plugin_name($plugin);
|
|
$plugin_repo = plugin_repo_for($ctx, $plugin);
|
|
require_repo($plugin_repo, "tribes-plugin-$plugin");
|
|
require_clean_repo($plugin_repo, "tribes-plugin-$plugin");
|
|
$plugin_commit = current_commit($plugin_repo, 'HEAD');
|
|
require_source_commit_pushed($ctx, $plugin_repo, "tribes-plugin-$plugin", $plugin_commit);
|
|
}
|
|
|
|
run_checked($ctx, 'git', '-C', $ctx->{guix_repo}, 'fetch', 'origin');
|
|
my $dev_ref = ensure_dev_branch_ready($ctx);
|
|
require_dev_branch_synced_to_master($ctx);
|
|
|
|
my @build_host_args = defined $ctx->{build_host} && length $ctx->{build_host}
|
|
? ('--build-host', $ctx->{build_host})
|
|
: ();
|
|
|
|
run_checked(
|
|
$ctx,
|
|
File::Spec->catfile($ctx->{guix_repo}, 'scripts', 'update-tribes-pin'),
|
|
'--guix-repo', $ctx->{guix_repo},
|
|
'--tribes-repo', $ctx->{tribes_repo},
|
|
@build_host_args,
|
|
$tribes_commit,
|
|
);
|
|
|
|
if (defined $plugin) {
|
|
run_checked(
|
|
$ctx,
|
|
File::Spec->catfile($ctx->{guix_repo}, 'scripts', 'update-plugin-pin'),
|
|
'--guix-repo', $ctx->{guix_repo},
|
|
'--tribes-repo', $ctx->{tribes_repo},
|
|
'--tribes-rev', $tribes_commit,
|
|
'--plugin-repo', $plugin_repo,
|
|
@build_host_args,
|
|
$plugin,
|
|
$plugin_commit,
|
|
);
|
|
}
|
|
|
|
my $message = defined $plugin
|
|
? "chore: update dev pins for $plugin plugin"
|
|
: 'chore: update dev Tribes pin';
|
|
commit_if_changed($ctx, $message);
|
|
authenticate_dev_branch($ctx);
|
|
run_checked($ctx, 'git', '-C', $ctx->{guix_repo}, 'push', 'origin', $DEV_BRANCH);
|
|
|
|
my $guix_commit = $ctx->{dry_run}
|
|
? current_commit($ctx->{guix_repo}, $dev_ref)
|
|
: current_commit($ctx->{guix_repo}, 'HEAD');
|
|
print "Dry run only; channel commit below is the current dev branch base.\n" if $ctx->{dry_run};
|
|
print_dev_env($guix_commit);
|
|
return $guix_commit;
|
|
}
|
|
|
|
sub reset_branch {
|
|
my ($ctx) = @_;
|
|
require_repo($ctx->{guix_repo}, 'guix-tribes');
|
|
require_clean_repo($ctx->{guix_repo}, 'guix-tribes');
|
|
ensure_dev_signing_key($ctx);
|
|
|
|
my $repo = $ctx->{guix_repo};
|
|
run_checked($ctx, 'git', '-C', $repo, 'fetch', 'origin');
|
|
branch_contains($repo, $INTRO_COMMIT, "origin/$DEV_BRANCH")
|
|
or fail("origin/$DEV_BRANCH does not descend from intro commit $INTRO_COMMIT.");
|
|
|
|
run_checked($ctx, 'git', '-C', $repo, 'checkout', '-B', $DEV_BRANCH, "origin/$DEV_BRANCH");
|
|
authenticate_dev_branch($ctx);
|
|
|
|
my $previous_dev_commit = current_commit($repo, 'HEAD');
|
|
my $master_commit = current_commit($repo, 'origin/master');
|
|
my $master_auth = capture_checked('git', '-C', $repo, 'show', 'origin/master:.guix-authorizations');
|
|
my $dev_key = capture_checked('git', '-C', $repo, 'show', "HEAD:$DEV_KEY_FILE");
|
|
my $sync_path = File::Spec->catfile($repo, '.supertest-dev-sync.json');
|
|
my $sync_data = {
|
|
mode => 'tree-sync',
|
|
source_branch => 'origin/master',
|
|
source_commit => $master_commit,
|
|
previous_dev_commit => $previous_dev_commit,
|
|
synced_at => iso_now(),
|
|
};
|
|
|
|
if ($ctx->{dry_run}) {
|
|
print "Would replace $DEV_BRANCH tree with origin/master $master_commit, then restore dev authorization and $DEV_KEY_FILE.\n";
|
|
run_checked($ctx, 'git', '-C', $repo, 'read-tree', '--reset', '-u', 'origin/master');
|
|
run_checked($ctx, 'git', '-C', $repo, 'add', '-A');
|
|
run_checked(
|
|
$ctx,
|
|
'git', '-C', $repo, 'commit', '-S' . $DEV_FINGERPRINT_COMPACT,
|
|
'-m', 'chore: sync supertest dev channel to master',
|
|
'-m', "Source: guix-tribes master $master_commit\nBase: previous supertest-dev $previous_dev_commit\nMode: tree sync, preserving dev channel authorization",
|
|
);
|
|
authenticate_dev_branch($ctx);
|
|
run_checked($ctx, 'git', '-C', $repo, 'push', '--force-with-lease', 'origin', $DEV_BRANCH);
|
|
return;
|
|
}
|
|
|
|
run_checked($ctx, 'git', '-C', $repo, 'read-tree', '--reset', '-u', 'origin/master');
|
|
write_file(File::Spec->catfile($repo, '.guix-authorizations'), ensure_dev_authorization($master_auth));
|
|
write_file(File::Spec->catfile($repo, $DEV_KEY_FILE), $dev_key);
|
|
write_json_file($sync_path, $sync_data);
|
|
run_checked($ctx, 'git', '-C', $repo, 'add', '-A');
|
|
run_checked(
|
|
$ctx,
|
|
'git', '-C', $repo, 'commit', '-S' . $DEV_FINGERPRINT_COMPACT,
|
|
'-m', 'chore: sync supertest dev channel to master',
|
|
'-m', "Source: guix-tribes master $master_commit\nBase: previous supertest-dev $previous_dev_commit\nMode: tree sync, preserving dev channel authorization",
|
|
);
|
|
authenticate_dev_branch($ctx);
|
|
run_checked($ctx, 'git', '-C', $repo, 'push', '--force-with-lease', 'origin', $DEV_BRANCH);
|
|
print "Synced $DEV_BRANCH tree to origin/master $master_commit.\n";
|
|
}
|
|
|
|
sub ensure_dev_authorization {
|
|
my ($text) = @_;
|
|
return $text if compact_fingerprint($text) =~ /\Q$DEV_FINGERPRINT_COMPACT\E/;
|
|
|
|
my $entry = qq( ("$DEV_FINGERPRINT"\n (name "$SIGNER_LABEL"))\n);
|
|
$text =~ s/\)\)\s*\z/$entry))/s
|
|
or fail('Could not add dev authorization to .guix-authorizations.');
|
|
return $text;
|
|
}
|
|
|
|
sub print_dev_env {
|
|
my ($commit) = @_;
|
|
print "SUPERTEST_DEV_CHANNEL_MODE=1\n";
|
|
print "SUPERTEST_GUIX_TRIBES_CHANNEL_URL=$CHANNEL_URL\n";
|
|
print "SUPERTEST_GUIX_TRIBES_CHANNEL_BRANCH=$DEV_BRANCH\n";
|
|
print "SUPERTEST_GUIX_TRIBES_CHANNEL_COMMIT=$commit\n";
|
|
print "SUPERTEST_GUIX_TRIBES_INTRO_COMMIT=$INTRO_COMMIT\n";
|
|
print "SUPERTEST_GUIX_TRIBES_INTRO_FINGERPRINT=$DEV_FINGERPRINT\n";
|
|
print "SUPERTEST_GUIX_TRIBES_SIGNER_FINGERPRINT=$DEV_FINGERPRINT\n";
|
|
print "SUPERTEST_GUIX_TRIBES_SIGNER_LABEL=$SIGNER_LABEL\n";
|
|
}
|
|
|
|
sub env_hash_for {
|
|
my ($ctx, $scenario, $guix_commit) = @_;
|
|
my %env = (
|
|
%ENV,
|
|
SUPERTEST_DEV_CHANNEL_MODE => '1',
|
|
SUPERTEST_GUIX_TRIBES_CHANNEL_URL => $CHANNEL_URL,
|
|
SUPERTEST_GUIX_TRIBES_CHANNEL_BRANCH => $DEV_BRANCH,
|
|
SUPERTEST_GUIX_TRIBES_CHANNEL_COMMIT => $guix_commit,
|
|
SUPERTEST_GUIX_TRIBES_INTRO_COMMIT => $INTRO_COMMIT,
|
|
SUPERTEST_GUIX_TRIBES_INTRO_FINGERPRINT => $DEV_FINGERPRINT,
|
|
SUPERTEST_GUIX_TRIBES_SIGNER_FINGERPRINT => $DEV_FINGERPRINT,
|
|
SUPERTEST_GUIX_TRIBES_SIGNER_LABEL => $SIGNER_LABEL,
|
|
SUPERTEST_CERT_MODE => $ctx->{cert_mode},
|
|
SUPERTEST_LEGION_REPO => $ctx->{legion_repo},
|
|
);
|
|
$env{SUPERTEST_KEEP_NODES} = '1' if $ctx->{keep_nodes};
|
|
$env{SUPERTEST_PLUGIN_NAME} = $ctx->{plugin} if defined $ctx->{plugin};
|
|
$env{SUPERTEST_RUN_ID} = $ctx->{run_id} if defined $ctx->{run_id};
|
|
return %env;
|
|
}
|
|
|
|
sub run_scenario {
|
|
my ($ctx, $scenario, $guix_commit) = @_;
|
|
defined $scenario && length $scenario or fail('Missing scenario name.');
|
|
|
|
my $run_id = $ctx->{run_id} // build_run_id();
|
|
$ctx->{run_id} = $run_id;
|
|
my $artifact_root = File::Spec->catdir($ctx->{root}, '.state', 'supertest', "$run_id-$scenario");
|
|
my $scenario_root = File::Spec->catdir($artifact_root, $scenario);
|
|
my $state_dir = File::Spec->catdir($scenario_root, 'legion-state');
|
|
my $cache_dir = File::Spec->catdir($scenario_root, 'legion-cache');
|
|
|
|
my %env = env_hash_for($ctx, $scenario, $guix_commit);
|
|
local %ENV = %env;
|
|
|
|
my $latest = {
|
|
scenario => $scenario,
|
|
runId => $run_id,
|
|
artifactRootDir => $artifact_root,
|
|
scenarioRootDir => $scenario_root,
|
|
legionStateDir => $state_dir,
|
|
legionCacheDir => $cache_dir,
|
|
legionRepo => $ctx->{legion_repo},
|
|
certMode => $ctx->{cert_mode},
|
|
devBranch => $DEV_BRANCH,
|
|
guixCommit => $guix_commit,
|
|
createdAt => iso_now(),
|
|
(defined $ctx->{plugin} ? (plugin => $ctx->{plugin}) : ()),
|
|
};
|
|
if ($ctx->{dry_run}) {
|
|
print "Would write latest run metadata:\n", encode_json($latest), "\n";
|
|
} else {
|
|
write_latest($ctx, $latest);
|
|
}
|
|
|
|
run_checked($ctx, 'devenv', 'shell', '--', 'npm', 'run', 'scenario', '--', $scenario);
|
|
}
|
|
|
|
sub build_run_id {
|
|
my $now = time();
|
|
my $millis = int(($now - int($now)) * 1000);
|
|
return strftime('%Y-%m-%dt%H%M%S', gmtime($now)) . sprintf('%03dz', $millis);
|
|
}
|
|
|
|
sub iso_now {
|
|
return strftime('%Y-%m-%dT%H:%M:%SZ', gmtime());
|
|
}
|
|
|
|
sub write_latest {
|
|
my ($ctx, $data) = @_;
|
|
my $dir = File::Spec->catdir($ctx->{root}, '.state', 'dev-branch');
|
|
make_path($dir);
|
|
write_file(File::Spec->catfile($dir, 'latest.json'), encode_json($data) . "\n");
|
|
}
|
|
|
|
sub read_latest {
|
|
my ($ctx) = @_;
|
|
my $path = File::Spec->catfile($ctx->{root}, '.state', 'dev-branch', 'latest.json');
|
|
-f $path or fail("No latest dev-branch run metadata found at $path.");
|
|
return decode_json(read_file($path));
|
|
}
|
|
|
|
sub run_legion_from_latest {
|
|
my ($ctx, @args) = @_;
|
|
my $latest = read_latest($ctx);
|
|
my %env = (
|
|
%ENV,
|
|
LEGION_STATE_DIR => $latest->{legionStateDir},
|
|
LEGION_CACHE_DIR => $latest->{legionCacheDir},
|
|
LEGION_APP_ROOT => $latest->{legionRepo},
|
|
LEGION_TEST_CERT_MODE => $latest->{certMode} // 'self-signed',
|
|
);
|
|
defined $ENV{LEGION_UNLOCK_PASSWORD}
|
|
or fail('LEGION_UNLOCK_PASSWORD is required for Legion ssh/rpc helpers.');
|
|
local %ENV = %env;
|
|
run_checked(
|
|
$ctx,
|
|
'devenv',
|
|
'shell',
|
|
'--',
|
|
'node',
|
|
'--import', 'tsx',
|
|
File::Spec->catfile($latest->{legionRepo}, 'src', 'engine', 'cli-main.ts'),
|
|
@args,
|
|
);
|
|
}
|
|
|
|
sub command_env {
|
|
my ($ctx) = @_;
|
|
require_repo($ctx->{guix_repo}, 'guix-tribes');
|
|
run_checked($ctx, 'git', '-C', $ctx->{guix_repo}, 'fetch', 'origin');
|
|
my $ref = branch_contains($ctx->{guix_repo}, $INTRO_COMMIT, "origin/$DEV_BRANCH")
|
|
? "origin/$DEV_BRANCH"
|
|
: $DEV_BRANCH;
|
|
my $commit = current_commit($ctx->{guix_repo}, $ref);
|
|
print_dev_env($commit);
|
|
}
|
|
|
|
sub command_ssh {
|
|
my ($ctx, $args) = @_;
|
|
my $node = shift @$args;
|
|
defined $node or fail('ssh requires a node reference.');
|
|
shift @$args if @$args && $args->[0] eq '--';
|
|
run_legion_from_latest($ctx, 'node', 'ssh', $node, @$args ? ('--', @$args) : ());
|
|
}
|
|
|
|
sub command_rpc {
|
|
my ($ctx, $args) = @_;
|
|
my $node = shift @$args;
|
|
defined $node or fail('rpc requires a node reference.');
|
|
shift @$args if @$args && $args->[0] eq '--';
|
|
@$args or fail('rpc requires an Elixir expression.');
|
|
run_legion_from_latest($ctx, 'node', 'rpc', $node, '--', join(' ', @$args));
|
|
}
|
|
|
|
sub main {
|
|
my ($command, $ctx, $args) = parse_args(@ARGV);
|
|
chdir $ctx->{root} or fail("Failed to chdir to $ctx->{root}: $!");
|
|
|
|
if ($command eq 'help') {
|
|
usage();
|
|
return 0;
|
|
}
|
|
if ($command eq 'reset') {
|
|
reset_branch($ctx);
|
|
return 0;
|
|
}
|
|
if ($command eq 'prepare') {
|
|
prepare($ctx);
|
|
return 0;
|
|
}
|
|
if ($command eq 'env') {
|
|
command_env($ctx);
|
|
return 0;
|
|
}
|
|
if ($command eq 'ssh') {
|
|
command_ssh($ctx, $args);
|
|
return 0;
|
|
}
|
|
if ($command eq 'rpc') {
|
|
command_rpc($ctx, $args);
|
|
return 0;
|
|
}
|
|
if ($command eq 'run') {
|
|
my $scenario = shift @$args;
|
|
@$args == 0 or fail("Unexpected extra arguments: @$args");
|
|
my $commit = prepare($ctx);
|
|
run_scenario($ctx, $scenario, $commit);
|
|
return 0;
|
|
}
|
|
|
|
fail("Unknown command: $command");
|
|
}
|
|
|
|
exit main();
|