Compare commits

..

1 Commits

Author SHA1 Message Date
eric sciple
fa07c78c0c . 2022-01-26 13:15:05 -06:00
12 changed files with 181 additions and 108 deletions

View File

@@ -1,19 +1,21 @@
## Features ## Features
- Add Runner Configuration option to disable auto update `--disableupdate` (#1558) - Bump runtime to dotnet 6 (#1471)
- Introduce `GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY` env variable to skip SSL Cert Verification on the Runner (#1616) - Show service container logs on teardown (#1563)
- Adds support for downloading trimmed versions of the runner when the entire package does not need to be upgraded (#1568)
## Bugs ## Bugs
- Set Outcome/Conclusion for composite action steps (#1600)
- Add masks for multiline secrets from ::add-mask:: (#1521)
- fix Log size and retention settings not work (#1507)
- Refactor SelfUpdater adding L0 tests. (#1564)
- Fix test failure: /bin/sleep on Macos 11 (Monterey) does not accept the suffix s. (#1472)
## Misc ## Misc
- Update `run.sh` to more gracefully handle updates (#1494) - Update dependency check for dotnet 6. (#1551)
- Use 8Mb default chunking for File Container Uploads (#1626) - Produce trimmed down runner packages. (#1556)
- Performance improvements in handling large amounts of live logs (#1592) - Deleted extra background in github-praph.png, which is displayed in README.md (#1432)
- Allow `./svc.sh stop` to exit as soon as runner process exits (#1580)
- Add additional tracing to help troubleshoot job message corruption (#1587)
## Windows x64 ## Windows x64

View File

@@ -1 +1 @@
2.287.1 <Update to ./src/runnerversion when creating release>

View File

@@ -0,0 +1,39 @@
@echo off
"%~dp0\bin\Runner.Listener.exe" run %*
rem using `if %ERRORLEVEL% EQU N` insterad of `if ERRORLEVEL N`
rem `if ERRORLEVEL N` means: error level is N or MORE
if %ERRORLEVEL% EQU 0 (
echo "Runner listener exit with 0 return code, stop the service, no retry needed."
exit /b 0
)
if %ERRORLEVEL% EQU 1 (
echo "Runner listener exit with terminated error, stop the service, no retry needed."
exit /b 0
)
if %ERRORLEVEL% EQU 2 (
echo "Runner listener exit with retryable error, re-launch runner in 5 seconds."
ping 127.0.0.1 -n 6 -w 1000 >NUL
exit /b 1
)
if %ERRORLEVEL% EQU 3 (
rem Sleep 5 seconds to wait for the runner update process finish
echo "Runner listener exit because of updating, re-launch runner in 5 seconds"
ping 127.0.0.1 -n 6 -w 1000 >NUL
exit /b 1
)
if %ERRORLEVEL% EQU 4 (
rem Sleep 5 seconds to wait for the ephemeral runner update process finish
echo "Runner listener exit because of updating, re-launch ephemeral runner in 5 seconds"
ping 127.0.0.1 -n 6 -w 1000 >NUL
exit /b 1
)
echo "Exiting after unknown error code: %ERRORLEVEL%"
exit /b 0

View File

@@ -0,0 +1,54 @@
#!/bin/bash
# Validate not sudo
user_id=`id -u`
if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then
echo "Must not run interactively with sudo"
exit 1
fi
# Run
shopt -s nocasematch
safe_sleep() {
if [ ! -x "$(command -v sleep)" ]; then
if [ ! -x "$(command -v ping)" ]; then
COUNT="0"
while [[ $COUNT != 5000 ]]; do
echo "SLEEP" > /dev/null
COUNT=$[$COUNT+1]
done
else
ping -c 5 127.0.0.1 > /dev/null
fi
else
sleep 5
fi
}
bin/Runner.Listener run $*
returnCode=$?
if [[ $returnCode == 0 ]]; then
echo "Runner listener exit with 0 return code, stop the service, no retry needed."
exit 0
elif [[ $returnCode == 1 ]]; then
echo "Runner listener exit with terminated error, stop the service, no retry needed."
exit 0
elif [[ $returnCode == 2 ]]; then
echo "Runner listener exit with retryable error, re-launch runner in 5 seconds."
safe_sleep
exit 1
elif [[ $returnCode == 3 ]]; then
# Sleep 5 seconds to wait for the runner update process finish
echo "Runner listener exit because of updating, re-launch runner in 5 seconds"
safe_sleep
exit 1
elif [[ $returnCode == 4 ]]; then
# Sleep 5 seconds to wait for the ephemeral runner update process finish
echo "Runner listener exit because of updating, re-launch ephemeral runner in 5 seconds"
safe_sleep
exit 1
else
echo "Exiting with unknown error code: ${returnCode}"
exit 0
fi

View File

@@ -13,21 +13,19 @@ if defined VERBOSE_ARG (
rem Unblock files in the root of the layout folder. E.g. .cmd files. rem Unblock files in the root of the layout folder. E.g. .cmd files.
powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command "$VerbosePreference = %VERBOSE_ARG% ; Get-ChildItem -LiteralPath '%~dp0' | ForEach-Object { Write-Verbose ('Unblock: {0}' -f $_.FullName) ; $_ } | Unblock-File | Out-Null" powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command "$VerbosePreference = %VERBOSE_ARG% ; Get-ChildItem -LiteralPath '%~dp0' | ForEach-Object { Write-Verbose ('Unblock: {0}' -f $_.FullName) ; $_ } | Unblock-File | Out-Null"
if /i "%~1" equ "localRun" (
rem ********************************************************************************
rem Local run.
rem ********************************************************************************
"%~dp0bin\Runner.Listener.exe" %*
) else (
rem ********************************************************************************
rem Run.
rem ********************************************************************************
"%~dp0bin\Runner.Listener.exe" run %*
rem Return code 4 means the run once runner received an update message. rem ********************************************************************************
rem Sleep 5 seconds to wait for the update process finish and run the runner again. rem Run.
if ERRORLEVEL 4 ( rem ********************************************************************************
timeout /t 5 /nobreak > NUL
"%~dp0bin\Runner.Listener.exe" run %* :launch_helper
) copy run-helper.cmd.template run-helper.cmd /Y
call "%~dp0run-helper.cmd" %*
if %ERRORLEVEL% EQU 1 (
echo "Restarting runner..."
goto :launch_helper
) else (
echo "Exiting runner..."
exit 0
) )

View File

@@ -1,12 +1,5 @@
#!/bin/bash #!/bin/bash
# Validate not sudo
user_id=`id -u`
if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then
echo "Must not run interactively with sudo"
exit 1
fi
# Change directory to the script root directory # Change directory to the script root directory
# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within # https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within
SOURCE="${BASH_SOURCE[0]}" SOURCE="${BASH_SOURCE[0]}"
@@ -16,49 +9,16 @@ while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symli
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done done
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
cp -f run-helper.sh.template run-helper.sh
# Do not "cd $DIR". For localRun, the current directory is expected to be the repo location on disk. # run the helper process which keep the listener alive
while :;
# Run do
shopt -s nocasematch "$DIR"/run-helper.sh $*
if [[ "$1" == "localRun" ]]; then
"$DIR"/bin/Runner.Listener $*
else
"$DIR"/bin/Runner.Listener run $*
# Return code 3 means the run once runner received an update message.
# Sleep 5 seconds to wait for the update process finish
returnCode=$? returnCode=$?
if [[ $returnCode == 3 ]]; then if [[ $returnCode == 1 ]]; then
if [ ! -x "$(command -v sleep)" ]; then echo "Restarting runner..."
if [ ! -x "$(command -v ping)" ]; then
COUNT="0"
while [[ $COUNT != 5000 ]]; do
echo "SLEEP" > /dev/null
COUNT=$[$COUNT+1]
done
else else
ping -c 5 127.0.0.1 > /dev/null echo "Exiting runner..."
exit 0
fi fi
else done
sleep 5
fi
elif [[ $returnCode == 4 ]]; then
if [ ! -x "$(command -v sleep)" ]; then
if [ ! -x "$(command -v ping)" ]; then
COUNT="0"
while [[ $COUNT != 5000 ]]; do
echo "SLEEP" > /dev/null
COUNT=$[$COUNT+1]
done
else
ping -c 5 127.0.0.1 > /dev/null
fi
else
sleep 5
fi
"$DIR"/bin/Runner.Listener run $*
else
exit $returnCode
fi
fi

View File

@@ -84,6 +84,7 @@ namespace GitHub.Runner.Common
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscape); this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift1); this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift1);
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift2); this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift2);
this.SecretMasker.AddValueEncoder(ValueEncoders.BashComparand);
this.SecretMasker.AddValueEncoder(ValueEncoders.CommandLineArgumentEscape); this.SecretMasker.AddValueEncoder(ValueEncoders.CommandLineArgumentEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.ExpressionStringEscape); this.SecretMasker.AddValueEncoder(ValueEncoders.ExpressionStringEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape); this.SecretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);

View File

@@ -54,7 +54,7 @@ namespace GitHub.Runner.Listener.Configuration
Trace.Info(nameof(LoadSettings)); Trace.Info(nameof(LoadSettings));
if (!IsConfigured()) if (!IsConfigured())
{ {
throw new InvalidOperationException("Not configured. Run config.(sh/cmd) to configure the runner."); throw new NonRetryableException("Not configured. Run config.(sh/cmd) to configure the runner.");
} }
RunnerSettings settings = _store.GetSettings(); RunnerSettings settings = _store.GetSettings();

View File

@@ -408,7 +408,7 @@ namespace GitHub.Runner.Listener
autoUpdateInProgress = true; autoUpdateInProgress = true;
var runnerUpdateMessage = JsonUtility.FromString<AgentRefreshMessage>(message.Body); var runnerUpdateMessage = JsonUtility.FromString<AgentRefreshMessage>(message.Body);
var selfUpdater = HostContext.GetService<ISelfUpdater>(); var selfUpdater = HostContext.GetService<ISelfUpdater>();
selfUpdateTask = selfUpdater.SelfUpdate(runnerUpdateMessage, jobDispatcher, !runOnce && HostContext.StartupType != StartupType.Service, HostContext.RunnerShutdownToken); selfUpdateTask = selfUpdater.SelfUpdate(runnerUpdateMessage, jobDispatcher, false, HostContext.RunnerShutdownToken);
Trace.Info("Refresh message received, kick-off selfupdate background process."); Trace.Info("Refresh message received, kick-off selfupdate background process.");
} }
else else

View File

@@ -38,7 +38,7 @@ namespace GitHub.Runner.Listener
private IRunnerServer _runnerServer; private IRunnerServer _runnerServer;
private int _poolId; private int _poolId;
private int _agentId; private int _agentId;
private readonly List<string> _updateTrace = new List<string>(); private readonly ConcurrentQueue<string> _updateTrace = new ConcurrentQueue<string>();
private Task _cloneAndCalculateContentHashTask; private Task _cloneAndCalculateContentHashTask;
private string _dotnetRuntimeCloneDirectory; private string _dotnetRuntimeCloneDirectory;
private string _externalsCloneDirectory; private string _externalsCloneDirectory;
@@ -80,7 +80,7 @@ namespace GitHub.Runner.Listener
} }
Trace.Info($"An update is available."); Trace.Info($"An update is available.");
_updateTrace.Add($"RunnerPlatform: {_targetPackage.Platform}"); _updateTrace.Enqueue($"RunnerPlatform: {_targetPackage.Platform}");
// Print console line that warn user not shutdown runner. // Print console line that warn user not shutdown runner.
await UpdateRunnerUpdateStateAsync("Runner update in progress, do not shutdown runner."); await UpdateRunnerUpdateStateAsync("Runner update in progress, do not shutdown runner.");
@@ -120,7 +120,7 @@ namespace GitHub.Runner.Listener
Trace.Info($"Delete old version runner backup."); Trace.Info($"Delete old version runner backup.");
stopWatch.Stop(); stopWatch.Stop();
// generate update script from template // generate update script from template
_updateTrace.Add($"DeleteRunnerBackupTime: {stopWatch.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"DeleteRunnerBackupTime: {stopWatch.ElapsedMilliseconds}ms");
await UpdateRunnerUpdateStateAsync("Generate and execute update script."); await UpdateRunnerUpdateStateAsync("Generate and execute update script.");
string updateScript = GenerateUpdateScript(restartInteractiveRunner); string updateScript = GenerateUpdateScript(restartInteractiveRunner);
@@ -145,14 +145,14 @@ namespace GitHub.Runner.Listener
totalUpdateTime.Stop(); totalUpdateTime.Stop();
_updateTrace.Add($"TotalUpdateTime: {totalUpdateTime.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"TotalUpdateTime: {totalUpdateTime.ElapsedMilliseconds}ms");
await UpdateRunnerUpdateStateAsync("Runner will exit shortly for update, should be back online within 10 seconds."); await UpdateRunnerUpdateStateAsync("Runner will exit shortly for update, should be back online within 10 seconds.");
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
{ {
_updateTrace.Add(ex.ToString()); _updateTrace.Enqueue(ex.ToString());
throw; throw;
} }
finally finally
@@ -260,9 +260,9 @@ namespace GitHub.Runner.Listener
} }
} }
_updateTrace.Add($"DownloadUrl: {packageDownloadUrl}"); _updateTrace.Enqueue($"DownloadUrl: {packageDownloadUrl}");
_updateTrace.Add($"RuntimeTrimmed: {runtimeTrimmed}"); _updateTrace.Enqueue($"RuntimeTrimmed: {runtimeTrimmed}");
_updateTrace.Add($"ExternalsTrimmed: {externalsTrimmed}"); _updateTrace.Enqueue($"ExternalsTrimmed: {externalsTrimmed}");
try try
{ {
@@ -328,14 +328,14 @@ namespace GitHub.Runner.Listener
if (fallbackToFullPackage) if (fallbackToFullPackage)
{ {
Trace.Error("Something wrong with the trimmed runner package, failback to use the full package for runner updates."); Trace.Error("Something wrong with the trimmed runner package, failback to use the full package for runner updates.");
_updateTrace.Add($"FallbackToFullPackage: {fallbackToFullPackage}"); _updateTrace.Enqueue($"FallbackToFullPackage: {fallbackToFullPackage}");
IOUtil.DeleteDirectory(latestRunnerDirectory, token); IOUtil.DeleteDirectory(latestRunnerDirectory, token);
Directory.CreateDirectory(latestRunnerDirectory); Directory.CreateDirectory(latestRunnerDirectory);
packageDownloadUrl = _targetPackage.DownloadUrl; packageDownloadUrl = _targetPackage.DownloadUrl;
packageHashValue = _targetPackage.HashValue; packageHashValue = _targetPackage.HashValue;
_updateTrace.Add($"DownloadUrl: {packageDownloadUrl}"); _updateTrace.Enqueue($"DownloadUrl: {packageDownloadUrl}");
try try
{ {
@@ -453,9 +453,9 @@ namespace GitHub.Runner.Listener
Trace.Info($"Download runner: finished download"); Trace.Info($"Download runner: finished download");
downloadSucceeded = true; downloadSucceeded = true;
stopWatch.Stop(); stopWatch.Stop();
_updateTrace.Add($"PackageDownloadTime: {stopWatch.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"PackageDownloadTime: {stopWatch.ElapsedMilliseconds}ms");
_updateTrace.Add($"Attempts: {attempt}"); _updateTrace.Enqueue($"Attempts: {attempt}");
_updateTrace.Add($"PackageSize: {downloadSize / 1024 / 1024}MB"); _updateTrace.Enqueue($"PackageSize: {downloadSize / 1024 / 1024}MB");
break; break;
} }
catch (OperationCanceledException) when (token.IsCancellationRequested) catch (OperationCanceledException) when (token.IsCancellationRequested)
@@ -505,7 +505,7 @@ namespace GitHub.Runner.Listener
stopWatch.Stop(); stopWatch.Stop();
Trace.Info($"Validated Runner Hash matches {archiveFile} : {packageHashValue}"); Trace.Info($"Validated Runner Hash matches {archiveFile} : {packageHashValue}");
_updateTrace.Add($"ValidateHashTime: {stopWatch.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"ValidateHashTime: {stopWatch.ElapsedMilliseconds}ms");
} }
} }
} }
@@ -561,7 +561,7 @@ namespace GitHub.Runner.Listener
stopWatch.Stop(); stopWatch.Stop();
Trace.Info($"Finished getting latest runner package at: {extractDirectory}."); Trace.Info($"Finished getting latest runner package at: {extractDirectory}.");
_updateTrace.Add($"PackageExtractTime: {stopWatch.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"PackageExtractTime: {stopWatch.ElapsedMilliseconds}ms");
} }
private Task CopyLatestRunnerToRoot(string latestRunnerDirectory, CancellationToken token) private Task CopyLatestRunnerToRoot(string latestRunnerDirectory, CancellationToken token)
@@ -594,7 +594,7 @@ namespace GitHub.Runner.Listener
} }
stopWatch.Stop(); stopWatch.Stop();
_updateTrace.Add($"CopyRunnerToRootTime: {stopWatch.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"CopyRunnerToRootTime: {stopWatch.ElapsedMilliseconds}ms");
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -720,9 +720,14 @@ namespace GitHub.Runner.Listener
_terminal.WriteLine(currentState); _terminal.WriteLine(currentState);
var traces = new List<string>(); var traces = new List<string>();
if (_updateTrace.Count > 0) while (_updateTrace.TryDequeue(out var trace))
{ {
foreach (var trace in _updateTrace) traces.Add(trace);
}
if (traces.Count > 0)
{
foreach (var trace in traces)
{ {
Trace.Info(trace); Trace.Info(trace);
} }
@@ -730,7 +735,7 @@ namespace GitHub.Runner.Listener
try try
{ {
await _runnerServer.UpdateAgentUpdateStateAsync(_poolId, _agentId, currentState, string.Join(Environment.NewLine, _updateTrace)); await _runnerServer.UpdateAgentUpdateStateAsync(_poolId, _agentId, currentState, string.Join(Environment.NewLine, traces));
_updateTrace.Clear(); _updateTrace.Clear();
} }
catch (VssResourceNotFoundException) catch (VssResourceNotFoundException)
@@ -806,7 +811,7 @@ namespace GitHub.Runner.Listener
finally finally
{ {
stopWatch.Stop(); stopWatch.Stop();
_updateTrace.Add($"{nameof(RestoreTrimmedExternals)}Time: {stopWatch.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"{nameof(RestoreTrimmedExternals)}Time: {stopWatch.ElapsedMilliseconds}ms");
} }
} }
@@ -858,7 +863,7 @@ namespace GitHub.Runner.Listener
finally finally
{ {
stopWatch.Stop(); stopWatch.Stop();
_updateTrace.Add($"{nameof(RestoreTrimmedDotnetRuntime)}Time: {stopWatch.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"{nameof(RestoreTrimmedDotnetRuntime)}Time: {stopWatch.ElapsedMilliseconds}ms");
} }
} }
@@ -889,7 +894,7 @@ namespace GitHub.Runner.Listener
var externalsHash = await HashFiles(externalsCloneDirectory, token); var externalsHash = await HashFiles(externalsCloneDirectory, token);
Trace.Info($"Externals content hash: {externalsHash}"); Trace.Info($"Externals content hash: {externalsHash}");
_contentHashes[_externals] = externalsHash; _contentHashes[_externals] = externalsHash;
_updateTrace.Add($"ExternalsHash: {_contentHashes[_externals]}"); _updateTrace.Enqueue($"ExternalsHash: {_contentHashes[_externals]}");
} }
else else
{ {
@@ -913,7 +918,7 @@ namespace GitHub.Runner.Listener
var runtimeHash = await HashFiles(dotnetRuntimeCloneDirectory, token); var runtimeHash = await HashFiles(dotnetRuntimeCloneDirectory, token);
Trace.Info($"Runtime content hash: {runtimeHash}"); Trace.Info($"Runtime content hash: {runtimeHash}");
_contentHashes[_dotnetRuntime] = runtimeHash; _contentHashes[_dotnetRuntime] = runtimeHash;
_updateTrace.Add($"DotnetRuntimeHash: {_contentHashes[_dotnetRuntime]}"); _updateTrace.Enqueue($"DotnetRuntimeHash: {_contentHashes[_dotnetRuntime]}");
} }
else else
{ {
@@ -983,7 +988,7 @@ namespace GitHub.Runner.Listener
finally finally
{ {
stopWatch.Stop(); stopWatch.Stop();
_updateTrace.Add($"{nameof(CloneDotnetRuntime)}Time: {stopWatch.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"{nameof(CloneDotnetRuntime)}Time: {stopWatch.ElapsedMilliseconds}ms");
} }
return false; return false;
@@ -1009,7 +1014,7 @@ namespace GitHub.Runner.Listener
finally finally
{ {
stopWatch.Stop(); stopWatch.Stop();
_updateTrace.Add($"{nameof(CloneExternals)}Time: {stopWatch.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"{nameof(CloneExternals)}Time: {stopWatch.ElapsedMilliseconds}ms");
} }
return Task.FromResult(false); return Task.FromResult(false);
@@ -1063,7 +1068,7 @@ namespace GitHub.Runner.Listener
} }
stopWatch.Stop(); stopWatch.Stop();
_updateTrace.Add($"{nameof(HashFiles)}{Path.GetFileName(fileFolder)}Time: {stopWatch.ElapsedMilliseconds}ms"); _updateTrace.Enqueue($"{nameof(HashFiles)}{Path.GetFileName(fileFolder)}Time: {stopWatch.ElapsedMilliseconds}ms");
return hashResult; return hashResult;
} }
} }

View File

@@ -38,6 +38,20 @@ namespace GitHub.DistributedTask.Logging
return Base64StringEscapeShift(value, 2); return Base64StringEscapeShift(value, 2);
} }
public static String BashComparand(String value)
{
var result = new StringBuilder();
foreach (var c in value)
{
if (!char.IsLowSurrogate(c))
{
result.Append('\\');
}
result.Append(c);
}
return result.ToString();
}
// Used when we pass environment variables to docker to escape " with \" // Used when we pass environment variables to docker to escape " with \"
public static String CommandLineArgumentEscape(String value) public static String CommandLineArgumentEscape(String value)
{ {

View File

@@ -1 +1 @@
2.287.1 2.286.0