Port the project settings and status UI for GitHub updates

What does this MR do and why?

Adds the project settings and status UI for GitHub updates (continuous sync from GitHub). Only projects imported while the github_continuous_import feature flag was on have the sync row this UI depends on.

  • Settings > Repository > GitHub updates: a Vue panel that shows the sync state of repository content (source) and of issues and merge requests (work items), with a badge, help text and the last update time.
  • Polling only while something is in progress: the page embeds the current status, so loading it makes no extra request. The panel polls the status endpoint only during setup (provisioning), while a row is pending or running, or while a stop runs (cutting_over). A settled project makes no requests. Polling follows Poll-Interval, pauses while the tab is hidden, and ignores responses sent before an action finished. If a poll fails, the next action or tab change starts it again.
  • Update now is enabled only when source and work items are current. It schedules a repair pass.
  • Stop GitHub updates is a toggle, enabled only in provider_canonical and cutover_failed. It asks for the project's full path through confirm_danger, and the stop endpoint isn't called before that. When a stop finishes while the page is open, the panel says to refresh the page to make changes in GitLab. After a stop, the panel warns when the final pass didn't complete.
  • A read-only banner appears on every project page while GitHub is the source of truth, except on Settings > Repository when the GitHub updates panel is shown there. Its text depends on the state (setting up, on, connection lost, stopping, stop failed) for users who can read GitHub updates. Signed-in users who can read the code also see the GitHub repository link. Other viewers, including anonymous users on public projects, see only that the project is read-only. Users who can update GitHub updates also get a link to the settings section.
  • Projects::Import::Sync::GithubUpdatesController serves status, update_now and stop. Errors return an error code (github_connection_lost, github_updates_not_stoppable, github_updates_not_updatable, github_updates_busy) and a message, which the UI shows. The settings page and the endpoints build the status with Import::Sync::GithubUpdatesStatusPresenter.
  • Access uses two new permissions: read_github_update (Maintainer and Auditor) gates status, and update_github_update (Maintainer) gates update_now, stop and the settings section. update_github_update is in the project:archived group, so archived and deletion-scheduled projects can't be updated or stopped.
  • A stop request writes a github_updates_stop_requested audit event, because it can't be undone. RequestStopService now takes current_user: for the audit author. Repeated requests while a stop is already running, and refused requests, write no event.
  • The settings section and endpoints depend only on the sync row, not on github_continuous_import. While the row exists the project stays read-only, so a flag rollback must not take away the way to stop updates.
  • CE change: when mirroring isn't available, Settings > Repository > Mirroring repositories now hides Add new and its form on every instance, instead of showing an empty form. While GitHub updates are on, the section says that mirroring isn't available because of them.

References

Closes #628593

Screenshots or screen recordings

Every string this MR adds, shown where it appears, for UX and technical writing review. Server messages are real responses from GDK with the database set to each state. Four are simulated in the browser: the two generic failure messages (request sent to a missing URL), the unknown-state fallback (status response rewritten), and the out-of-date notice (page clock moved forward, so one timestamp reads 40 minutes ago).

Read-only banner on the project pages
Case Screenshot
Setting up banner-setting-up
Updates on banner-on
Stopping banner-stopping
Stop failed banner-stop-failed
Connection lost banner-disconnected
Settings panel, by state
Case Screenshot
Loading (before the page script runs) panel-loading
Setting up panel-setting-up
Updates on: one row delayed by the rate limit, one paused panel-delayed-paused
Updates on: a row in error, Update now enabled to retry it 3-row-error
Stopping panel-stopping
Stop failed panel-stop-failed
Connection lost panel-disconnected
Stopped before the final check finished panel-stopped-incomplete
A state the page doesn't recognize (fallback) panel-unknown-state
Stop confirmation
Case Screenshot
Confirmation dialog stop-modal
Messages after Update now
Case Screenshot
Update scheduled notice-update-scheduled
Refused: updates are not on (state changed since the page loaded) notice-update-inactive
Request failed (generic fallback) notice-update-failed
Messages after Stop GitHub updates
Case Screenshot
Import still running notice-stop-import-running
Not in a state that can be stopped notice-stop-invalid-state
Connection lost notice-stop-connection-lost
Another update is still finishing notice-stop-lease-busy
Request failed (generic fallback) notice-stop-failed
Status polling
Case Screenshot
Status request failed twice notice-status-failed
No change for 5 minutes, polling stopped notice-status-outdated
Mirroring repositories
Case Screenshot
While GitHub updates are on mirroring-unavailable

How to set up and validate locally

Prerequisites

  1. Configure GitHub OAuth in GDK:
    1. On GitHub, go to Settings > Developer settings > OAuth Apps > New OAuth App. Set Homepage URL to your GDK URL (for example, https://gdk.test:3000) and Authorization callback URL to https://gdk.test:3000/users/auth. This path covers both GitHub sign-in (/users/auth/github/callback) and the import (/users/auth/-/import/github/callback), because GitHub accepts any redirect under the callback URL. Generate a client secret.

    2. Add the app to gdk.yml, then run gdk reconfigure and gdk restart:

      omniauth:
        github:
          enabled: true
          client_id: '<client ID>'
          client_secret: '<client secret>'
    3. In a Rails console, Gitlab::Auth::OAuth::Provider.enabled?(:github) returns true.

  2. Enable the feature flag before you authenticate with GitHub: Feature.enable(:github_continuous_import). GitLab stores the GitHub credential for updates only during the OAuth sign-in while the flag is on.
  3. Have a group on a Premium or Ultimate license to import into. A personal namespace doesn't offer updates.

Import a project with GitHub updates on

  1. Go to New project > Import project > GitHub and select Authenticate with GitHub. Updates aren't offered when you import with a personal access token.
  2. Authorize the OAuth app. If you authenticated before enabling the flag, the import fails with "To import with updates on, authorize GitHub again." Authenticate with GitHub again.
  3. Select Advanced import settings, then Keep this project updated from GitHub.
  4. Select the Premium or Ultimate group as the target and select Import.
  5. When the import finishes, check the sync row in a Rails console: Project.find_by_full_path('<group>/<project>').import_sync_repository.authority_state returns "provider_canonical".

GitHub can't reach a local GDK with webhooks, so changes made on GitHub reach GitLab only through the periodic repair pass or Update now.

Validate

  1. Go to Settings > Repository > GitHub updates. Both rows show Up to date, Update now is enabled, and the browser makes no status requests. Any project page shows the read-only banner, and its link opens the GitHub updates section. The GitHub repository link shows the external link icon.
  2. Select Update now. The rows show Update scheduled…, and the panel polls the status every 5 seconds until both rows are Up to date again. Then polling stops and Update now is enabled.
  3. In a Rails console, put a row in error: Import::Sync::Repository.find_by!(project_id: <id>).sync_states.find_by!(domain: :work_items).update!(state: :degraded). Refresh the page. The row shows Error, and Update now is enabled. Select it to retry.
  4. In a Rails console, leave a row pending: Import::Sync::Repository.find_by!(project_id: <id>).sync_states.find_by!(domain: :work_items).update!(state: :pending). Refresh the page. The panel polls every 5 seconds, then stops after 5 minutes without a change and shows an info alert that the status might be out of date. Switch to another browser tab and back: the alert goes away and polling starts again.
  5. In a Rails console, set the row to disconnected: Import::Sync::Repository.find_by!(project_id: <id>).disconnect!(:token_revoked). Refresh the page. The panel shows the lost connection, Stop GitHub updates is disabled, and after a refresh the banner says GitLab lost its connection to GitHub. Restore it with .restore!.
  6. On a throwaway project (a stop can't be undone), select Stop GitHub updates. The text above the button says stopping is permanent and that you must import the repository again to get updates back. The confirmation keeps Stop updates disabled until you type the full path. The button shows a spinner and the panel follows the stop until it finishes. Then a success alert says to refresh the page to make changes in GitLab, Stop GitHub updates and Update now are gone, and polling stops. Open any other project page: the banner is gone. The project's Secure > Audit events shows the stop request with your user (needs a license with audit events).
  7. Archive the project, or schedule it for deletion: update_now and stop return 404.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Rodrigo Tomonari

Merge request reports

Loading
Loading