Skip to content
62 changes: 62 additions & 0 deletions app_dart/docs/cicd_flowchart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# CICD Label Flowchart

This flowchart describes the logic for managing the `CICD` label in Cocoon for running presubmit CI.

```mermaid
Comment thread
eyebrowsoffire marked this conversation as resolved.
flowchart TD
PR_Opened([Event: PR Opened]) --> Is_Draft{Is Draft?}

Is_Draft -- No --> Is_Privileged_Open{Is Privileged?}
Is_Draft -- Yes --> Create_Awaiting[Create Awaiting Check Run]

Is_Privileged_Open -- Yes --> Add_Label[Add CICD Label]
Add_Label --> Start_Pre[Start Presubmits]

Is_Privileged_Open -- No --> Create_Awaiting

Create_Awaiting --> State_Awaiting((State: Awaiting))
Start_Pre --> State_Running((State: Running))

State_Awaiting --> Label_Added([Event: CICD Label Added])
Label_Added --> Resolve_Awaiting[Resolve Awaiting Check Run]
Resolve_Awaiting --> Start_Pre

State_Running --> Changes_Pushed([Event: Changes Pushed])
Changes_Pushed --> Is_Privileged_Push{Is Privileged?}
Remove_Label --> Create_Awaiting

Is_Privileged_Push -- No --> Remove_Label[Remove CICD Label]

Is_Privileged_Push -- Yes --> Label_Present{Has CICD Label?}
Label_Present -- Yes --> Start_Pre
Label_Present -- No --> Create_Awaiting
```

## Alternative State Machine Diagram (PlantUML)

```plantuml
@startuml
state PR {
state Waiting
Waiting : entry / Create Awaiting Check Run
Waiting : exit / resolve(awaiting check)

state Running
Running: entry/ Start Presubmits

state if_priv2 <<choice>>
Running --> if_priv2: onPushed
if_priv2 --> Running: isPrivileged && labeled(CICD)
if_priv2 --> Waiting : default: remove(CICD)

state if_draft <<choice>>
PR --> if_draft: openned

if_draft --> Waiting : isDraft
if_draft --> Running: isPrivileged
if_draft --> Waiting : default

Waiting --> Running: labeled(CICD)
}
@enduml
```
1 change: 1 addition & 0 deletions app_dart/lib/cocoon_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ export 'src/service/flags/dynamic_config.dart';
export 'src/service/gerrit_service.dart';
export 'src/service/github_checks_service.dart';
export 'src/service/luci_build_service.dart';
export 'src/service/pull_request_manager.dart';
export 'src/service/scheduler.dart';
91 changes: 91 additions & 0 deletions app_dart/lib/src/model/firestore/pull_request_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2026 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';

import 'package:github/github.dart';
import 'package:googleapis/firestore/v1.dart';

import '../../service/firestore.dart';
import 'base.dart';

/// Represents the state of a Pull Request that we want to persist across events.
final class PullRequestState extends AppDocument<PullRequestState> {
static const kCollectionId = 'pullRequestStates';
static const kIsPrivilegedField = 'is_privileged';
static const kLatestShaField = 'latest_sha';
static const kScheduledShaField = 'scheduled_sha';
static const kSlugField = 'slug';
static const kNumberField = 'number';

@override
AppDocumentMetadata<PullRequestState> get runtimeMetadata => metadata;

/// Description of the document in Firestore.
static final metadata = AppDocumentMetadata<PullRequestState>(
collectionId: kCollectionId,
fromDocument: PullRequestState.fromDocument,
);

/// Create [PullRequestState] from a Document.
static PullRequestState fromDocument(Document doc) {
return PullRequestState()
..fields = doc.fields!
..name = doc.name!;
}

/// Whether the PR author is a privileged user (roller or flutter-hacker).
bool? get isPrivileged => fields[kIsPrivilegedField]?.booleanValue;

set isPrivileged(bool? value) {
if (value != null) {
fields[kIsPrivilegedField] = value.toValue();
} else {
fields.remove(kIsPrivilegedField);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know when we'd use it, but would you want:

        } else {
          fields.remove(kIsPrivilegedField);
        }

We could do that for kLatestShaField and kScheduledShaField as well - again, not sure when we'd use it, but maybe worth having?

}

/// The latest SHA that we have processed for this PR.
String? get latestSha => fields[kLatestShaField]?.stringValue;

set latestSha(String? value) {
if (value != null) {
fields[kLatestShaField] = value.toValue();
} else {
fields.remove(kLatestShaField);
}
}

/// The SHA for which we have scheduled presubmits.
String? get scheduledSha => fields[kScheduledShaField]?.stringValue;

set scheduledSha(String? value) {
if (value != null) {
fields[kScheduledShaField] = value.toValue();
} else {
fields.remove(kScheduledShaField);
}
}

/// The repository slug associated with the pull request.
RepositorySlug get slug => RepositorySlug.fromJson(
json.decode(fields[kSlugField]!.stringValue!) as Map<String, Object?>,
);

set slug(RepositorySlug value) {
fields[kSlugField] = json.encode(value.toJson()).toValue();
Comment thread
eyebrowsoffire marked this conversation as resolved.
}

/// The PR number.
int get number => int.parse(fields[kNumberField]!.integerValue!);

set number(int value) {
fields[kNumberField] = value.toValue();
}

/// Generates the document ID for a PR.
static String getDocumentId(RepositorySlug slug, int number) {
return '${slug.owner}\u001F${slug.name}\u001F$number';
}
}
Loading
Loading