Update. One answer at the top - up to date, an update waiting, one running, * the database still to update - with the one button that acts on it, then what the update * brings and what to do first, and this install's details alongside. * * This page does not run git. cron/update_check.php does the fetch on its own schedule and * stores what it found; everything below is a read of that, plus .git for the local commit. * Check Now and Update ITFlow ask the dispatcher for a run rather than working inside the * request, so the page works the same on a host whose web PHP cannot run external commands. * * Every settings column it reads is checked for first. This page has to render on an install * whose files are newer than its schema, because that is the state it exists to get people * out of - mysqli throws on an unknown column, and the page would die before its button drew. */ $repo_branch = getRepoBranch(); $current_version = gitCurrentCommit(); $is_git_checkout = gitDir() !== ''; $update_check_available = settingsColumnExists($mysqli, 'config_update_latest_commit'); $update_details_available = settingsColumnExists($mysqli, 'config_update_details'); $update_queue_available = settingsColumnExists($mysqli, 'config_update_queued_at'); $latest_version = ''; $update_checked_at = null; $pending_commits = []; $check_job = null; $details = []; if ($update_check_available) { $update_check_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_update_latest_commit, config_update_pending_commits, config_update_checked_at" . ($update_details_available ? ", config_update_details" : '') . " FROM settings WHERE company_id = 1")); $latest_version = (string) ($update_check_row['config_update_latest_commit'] ?? ''); $update_checked_at = $update_check_row['config_update_checked_at'] ?? null; // Stored as JSON by the job: [[short hash, ISO date, subject], ...]. Rebuilt element by // element rather than trusted wholesale - it is the shape the list indexes into $stored_commits = json_decode((string) ($update_check_row['config_update_pending_commits'] ?? ''), true); if (is_array($stored_commits)) { foreach ($stored_commits as $stored_commit) { if (is_array($stored_commit) && count($stored_commit) === 3) { $pending_commits[] = array_map('strval', array_values($stored_commit)); } } } $details = json_decode((string) ($update_check_row['config_update_details'] ?? ''), true); $details = is_array($details) ? $details : []; $check_job = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT cron_job_run_now, cron_job_last_run_at, cron_job_last_status, cron_job_last_error, cron_job_last_error_at FROM cron_jobs WHERE cron_job_name = 'update_check' LIMIT 1")); } // What the check found besides the commits (updateCheckDetails()) - each part checked for shape $latest_release = is_string($details['version'] ?? null) ? $details['version'] : ''; $changelog = []; foreach ((array) ($details['changelog'] ?? []) as $section) { if (is_array($section) && count($section) === 2 && is_string($section[0]) && is_string($section[1])) { $changelog[] = $section; } } $local_changes = array_values(array_filter((array) ($details['local_changes'] ?? []), 'is_string')); $local_changes_count = intval($details['local_changes_count'] ?? count($local_changes)); $details_known = isset($details['local_changes_count']); $update_queued_at = null; $cron_last_dispatch_at = null; $update_job = null; if ($update_queue_available) { $update_queued_at = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_update_queued_at FROM settings WHERE company_id = 1"))['config_update_queued_at'] ?? null; $update_job = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT cron_job_last_run_at, cron_job_last_finished_at, cron_job_last_status, cron_job_last_error, cron_job_last_error_at FROM cron_jobs WHERE cron_job_name = 'app_update' LIMIT 1")); } // The dispatcher's heartbeat - queueing is only worth offering if something will pick it up if (settingsColumnExists($mysqli, 'config_cron_last_dispatch_at')) { $cron_last_dispatch_at = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_cron_last_dispatch_at FROM settings WHERE company_id = 1"))['config_cron_last_dispatch_at'] ?? null; } $cron_is_running = !empty($config_enable_cron) && !empty($cron_last_dispatch_at) && strtotime($cron_last_dispatch_at) > strtotime('-5 minutes'); // version_compare, not > - "2.6.10" is less than "2.6.9" as a plain string comparison $db_update_available = version_compare(LATEST_DATABASE_VERSION, CURRENT_DATABASE_VERSION, '>'); // Derived from the two commits, not from the list: a force-push or a local commit leaves the // working tree at a different place with nothing to list, and that is still "not up to date" $app_update_available = $latest_version !== '' && $current_version !== '' && $latest_version !== $current_version; // The dispatcher clears run_now inside the same UPDATE that claims the job, so this is true // for exactly as long as the request is outstanding // A job left at Running long past its time died without being recorded - not still going $job_running = function ($job, $minutes) { return ($job['cron_job_last_status'] ?? '') === 'Running' && !empty($job['cron_job_last_run_at']) && strtotime($job['cron_job_last_run_at']) > strtotime("-$minutes minutes"); }; $check_in_progress = !empty($check_job['cron_job_run_now']) || $job_running($check_job, 10); $check_failed = ($check_job['cron_job_last_status'] ?? '') === 'Failed' && !empty($check_job['cron_job_last_error']); $check_stopped = str_starts_with((string) ($check_job['cron_job_last_status'] ?? ''), 'Stopped: ') ? substr((string) $check_job['cron_job_last_status'], 9) : ''; $update_status = (string) ($update_job['cron_job_last_status'] ?? ''); $update_running = !empty($update_queued_at) || $job_running($update_job, 30); $update_failed = !$update_running && $update_status === 'Failed' && !empty($update_job['cron_job_last_error']); $update_finished = !$update_running && $update_status === 'Completed' && !empty($update_job['cron_job_last_finished_at']) && strtotime($update_job['cron_job_last_finished_at']) > strtotime('-1 hour'); // A release number to show when the branch moves by release (master), or nothing to compare $new_release = $latest_release !== '' && version_compare($latest_release, APP_VERSION, '>') ? $latest_release : ''; $commit_count = count($pending_commits); $has_upgrading_notes = (bool) array_filter($changelog, function ($section) { return preg_match('/^###\s+Upgrading/mi', $section[1]) === 1; }); /* * The one thing to say at the top. In this order, because each hides the ones after it: an * update already on its way, a check on its way, a database behind its code (parts of ITFlow * error until it catches up), an update waiting, then the quiet states. */ if ($update_running) { $state = 'updating'; } elseif ($check_in_progress) { $state = 'checking'; } elseif ($db_update_available) { $state = 'database'; } elseif ($app_update_available) { $state = 'available'; } elseif (!$is_git_checkout) { $state = 'manual'; } elseif (empty($update_checked_at)) { $state = 'unknown'; } else { $state = 'current'; } // Update ITFlow where there is something to update (or no telling), Check where a check can run $show_update_button = in_array($state, ['database', 'available', 'unknown'], true) && $update_queue_available; $show_check_button = in_array($state, ['available', 'unknown', 'current'], true) && $update_check_available && $is_git_checkout; $queue_url = "post.php?queue_update&csrf_token=" . urlencode($_SESSION['csrf_token']); $check_url = "post.php?check_update&csrf_token=" . urlencode($_SESSION['csrf_token']); // What the confirmation says - the parts that differ from one install to the next first $confirm_text = "Cron updates ITFlow's files and then its database in the background, within a minute."; if ($local_changes_count > 0) { $confirm_text .= " The $local_changes_count file" . ($local_changes_count === 1 ? '' : 's') . " changed on this install will be put back to the released version" . ($local_changes_count === 1 ? '' : 's') . "."; } $confirm_text .= " Make sure you have a backup first."; // The command line, as this server runs it - the user that owns the files, in this folder $install_root = dirname(__DIR__); $file_owner = ''; if (function_exists('posix_getpwuid')) { $file_owner = (string) (posix_getpwuid((int) @fileowner($install_root . '/scripts/update_cli.php'))['name'] ?? ''); } $shell_word = function ($word) { return preg_match('/^[A-Za-z0-9_\/.@%+=:,-]+$/', $word) ? $word : escapeshellarg($word); }; $cli_command = "cd " . $shell_word($install_root) . "\n" . ($file_owner !== '' ? "sudo -u " . $shell_word($file_owner) . " " : '') . "php scripts/update_cli.php"; $hero = [ 'updating' => ['icon' => 'fa-sync-alt fa-spin', 'tone' => 'primary', 'title' => 'Updating ITFlow'], 'checking' => ['icon' => 'fa-circle-notch fa-spin', 'tone' => 'secondary', 'title' => 'Checking for updates'], 'database' => ['icon' => 'fa-database', 'tone' => 'warning', 'title' => 'Finish updating ITFlow'], 'available' => ['icon' => 'fa-arrow-alt-circle-up', 'tone' => 'primary', 'title' => $new_release !== '' ? "ITFlow $new_release is available" : 'An update is available'], 'manual' => ['icon' => 'fa-box-open', 'tone' => 'secondary', 'title' => 'This install is updated by hand'], 'unknown' => ['icon' => 'fa-question', 'tone' => 'secondary', 'title' => 'ITFlow has not checked for updates yet'], 'current' => ['icon' => 'fa-check', 'tone' => 'success', 'title' => 'ITFlow is up to date'], ][$state]; ?>
The last update did not finish

It stopped . A database update that fails part way stops without recording its version, so once the cause is fixed it is safe to run the update again.

The forum can help - include the output above.

ITFlow was updated - this is ' . escapeHtml(substr($current_version, 0, 7)) . '' : '' ?>.
The last update check did not finish

What this page says about updates is from the check before it, so it may be out of date. Check that Git is installed and that this server can reach GitHub.

>

Requested . Started . Cron updates the files and then the database in the background - usually a minute or two. You can leave this page; you'll get a notification when it's done.

Cron hasn't run in the last five minutes, or it's switched off, so nothing will pick the update up until it does. See Maintenance > Cron.

Cron checks within a minute; this page refreshes itself.

Cron hasn't run in the last five minutes, or it's switched off, so the check won't happen until it does. See Maintenance > Cron.

ITFlow's files are newer than its database - the database is at and these files need . Parts of ITFlow may show errors until it catches up. A newer version is waiting too, and comes with it.

You're on . change on since this copy. See what's new below, and take a backup first.

It wasn't installed with Git, so it can't fetch new versions itself. Put the new release's files in place by hand, as the docs describe - this page then updates the database.

The last check stopped: . The Update Check job does it every night.

You're on . Everything is going to be alright.

While you're here - when did you last check that your backup restores?

This install has to be updated from a shell once. Updating from this page needs a database change it hasn't caught up with yet. Run the command under From the command line, and this page works from then on.
Cron hasn't run in the last five minutes, or it's switched off, so an update would wait until it does. Check Maintenance > Cron, or update from the command line.
Take a backup
A snapshot of the whole server is safest - you can roll back in a minute. Otherwise make a Full Backup in Maintenance > Backup and copy it off the server.
Read what's changing
Each release's Upgrading notes below say whether it needs a step of its own - ITFlow won't do those for you. What's new is below. Some releases need a step of their own, and say so. Some releases need a step of their own - the changelog says.
0) { ?>
of ITFlow's files been changed on this install
Updating puts back the released version, so lost. Your config.php, uploads and custom folders are never touched.
Which files
  • count($local_changes)) { ?>
  • and more
Nothing of yours is overwritten
None of ITFlow's own files have been changed on this install. Your config.php, uploads and custom folders are never touched.
Pick a quiet time
Your team may see errors for a minute or two while it runs.

What's new

[$section_version, $section_markdown]) { ?>

The version you're on, with changes since

The release notes are in the changelog. Check again to see them here.

This install

  • Version → ' . escapeHtml($new_release) . '' : '' ?>
  • Updates from Not the release branch - for test copies
  • Database → ' . escapeHtml(LATEST_DATABASE_VERSION) . '' : '' ?>
  • Commit —' : '' . escapeHtml(substr($current_version, 0, 7)) . '' ?>
  • Last checked Never
  • Last updated Failed Not from this page yet
  • Cron

From the command line

Does the same as - the files, then the database - without waiting for cron: Updates the database once the new release's files are in place:

Add --update_db to update only the database - after restoring an older backup, say. See Updating ITFlow.