Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ require:

When this setting is present on a repository that belongs to a single user (instead of an organization), only the repository owner is allowed to push commits without a sign-off.

### Override action

By default, failed DCO checks include a `Set DCO to pass` button in the check details page.

To hide this button, set the following option in the [configuration file](docs/config/dco.yml) (`.github/dco.yml`):

```yaml
allowOverrideAction: false
```

> [!NOTE]
> This option only hides DCO2's own override action. It is not a security
> boundary because repository maintainers with write access can still change
> the repository configuration.

## Deployment

We'll soon provide some documentation about how to deploy your own instance of DCO2.
Expand Down
22 changes: 17 additions & 5 deletions dco2/src/dco/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ async fn process_check_run_event(gh_client: DynGHClient, event: &CheckRunEvent)
if let Some(requested_action) = &event.requested_action
&& requested_action.identifier == OVERRIDE_ACTION_IDENTIFIER
{
// Ignore override requests when the repository disabled this action
let config = gh_client
.get_config(&ctx)
.await
.context("error getting repository configuration")?
.unwrap_or_default();
if !config.override_action_is_allowed() {
return Ok(());
}

let check_run = CheckRun::new(NewCheckRunInput {
actions: vec![],
completed_at: Utc::now(),
Expand Down Expand Up @@ -158,15 +168,17 @@ async fn process_pull_request_event(gh_client: DynGHClient, event: &PullRequestE
let (conclusion, title, actions) = if output.num_commits_with_errors == 0 {
(CheckRunConclusion::Success, CHECK_PASSED_TITLE, vec![])
} else {
(
CheckRunConclusion::ActionRequired,
CHECK_FAILED_TITLE,
let actions = if output.config.override_action_is_allowed() {
vec![CheckRunAction {
label: OVERRIDE_ACTION_LABEL.to_string(),
description: OVERRIDE_ACTION_DESCRIPTION.to_string(),
identifier: OVERRIDE_ACTION_IDENTIFIER.to_string(),
}],
)
}]
} else {
vec![]
};

(CheckRunConclusion::ActionRequired, CHECK_FAILED_TITLE, actions)
};
let check_run = CheckRun::new(NewCheckRunInput {
actions,
Expand Down
116 changes: 116 additions & 0 deletions dco2/src/dco/event/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ async fn check_run_event_requested_action_override_error_creating_check_run() {
};

let mut gh_client = MockGHClient::new();
gh_client
.expect_get_config()
.with(eq(event.ctx()))
.times(1)
.returning(|_| Box::pin(future::ready(Ok(Some(Config::default())))));
let expected_ctx = event.ctx();
gh_client
.expect_create_check_run()
Expand All @@ -109,6 +114,36 @@ async fn check_run_event_requested_action_override_error_creating_check_run() {
process_event(Arc::new(gh_client), &Event::CheckRun(event)).await.unwrap();
}

#[tokio::test]
async fn check_run_event_requested_action_override_ignored_when_disabled() {
let event = CheckRunEvent {
action: CheckRunEventAction::RequestedAction,
check_run: CheckRunEventCheckRun {
head_sha: "head_sha".to_string(),
},
installation: Installation { id: 1 },
repository: Repository {
name: "repo".to_string(),
owner: RepositoryOwner {
login: "owner".to_string(),
},
},
requested_action: Some(RequestedAction {
identifier: OVERRIDE_ACTION_IDENTIFIER.to_string(),
}),
};

let mut gh_client = MockGHClient::new();
gh_client.expect_get_config().with(eq(event.ctx())).times(1).returning(|_| {
Box::pin(future::ready(Ok(Some(Config {
allow_override_action: Some(false),
..Default::default()
}))))
});

process_event(Arc::new(gh_client), &Event::CheckRun(event)).await.unwrap();
}

#[tokio::test]
async fn check_run_event_requested_action_override_success() {
let event = CheckRunEvent {
Expand All @@ -129,6 +164,11 @@ async fn check_run_event_requested_action_override_success() {
};

let mut gh_client = MockGHClient::new();
gh_client
.expect_get_config()
.with(eq(event.ctx()))
.times(1)
.returning(|_| Box::pin(future::ready(Ok(Some(Config::default())))));
let expected_ctx = event.ctx();
gh_client
.expect_create_check_run()
Expand Down Expand Up @@ -1019,3 +1059,79 @@ async fn pull_request_event_opened_action_success_check_failed() {

process_event(Arc::new(gh_client), &Event::PullRequest(event)).await.unwrap();
}

#[tokio::test]
async fn pull_request_event_opened_action_success_check_failed_override_action_disabled() {
let event = PullRequestEvent {
action: PullRequestEventAction::Opened,
installation: Installation { id: 1 },
organization: None,
pull_request: PullRequest {
base: PullRequestBase {
ref_: "base_ref".to_string(),
sha: "base_sha".to_string(),
},
head: PullRequestHead {
ref_: "head_ref".to_string(),
sha: "head_sha".to_string(),
},
html_url: "url".to_string(),
},
repository: Repository {
name: "repo".to_string(),
owner: RepositoryOwner {
login: "owner".to_string(),
},
},
};

let mut gh_client = MockGHClient::new();
gh_client
.expect_compare_commits()
.with(eq(event.ctx()), eq("base_sha"), eq("head_sha"))
.times(1)
.returning(|_, _, _| {
Box::pin(future::ready(Ok(vec![Commit {
author: Some(User {
name: "user1".to_string(),
email: "user1@email.test".to_string(),
..Default::default()
}),
committer: Some(User {
name: "user1".to_string(),
email: "user1@email.test".to_string(),
..Default::default()
}),
message: indoc! {r"
Test commit message

Signed-off-by: userx <userx@email.test>
"}
.to_string(),
..Default::default()
}])))
});
gh_client.expect_get_config().with(eq(event.ctx())).times(1).returning(|_| {
Box::pin(future::ready(Ok(Some(Config {
allow_override_action: Some(false),
..Default::default()
}))))
});
let expected_ctx = event.ctx();
gh_client
.expect_create_check_run()
.withf(move |ctx, check_run| {
*ctx == expected_ctx
&& check_run.actions().is_empty()
&& check_run.completed_at() >= check_run.started_at()
&& check_run.conclusion() == &CheckRunConclusion::ActionRequired
&& check_run.head_sha() == "head_sha"
&& check_run.name() == CHECK_NAME
&& check_run.status() == &CheckRunStatus::Completed
&& check_run.title() == CHECK_FAILED_TITLE
})
.times(1)
.returning(|_, _| Box::pin(future::ready(Ok(()))));

process_event(Arc::new(gh_client), &Event::PullRequest(event)).await.unwrap();
}
10 changes: 9 additions & 1 deletion dco2/src/github/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,21 +428,24 @@ impl From<octorust::types::CommitDataType> for Commit {
}

/// Default values for the configuration.
pub const DEFAULT_OVERRIDE_ACTION_ALLOWED: bool = true;
pub const DEFAULT_MEMBERS_SIGNOFF_REQUIRED: bool = true;
pub const DEFAULT_INDIVIDUAL_REMEDIATION_COMMITS_ALLOWED: bool = false;
pub const DEFAULT_THIRD_PARTY_REMEDIATION_COMMITS_ALLOWED: bool = false;
pub const DEFAULT_MEMBERS_SIGNOFF_REQUIRED: bool = true;

/// Repository configuration.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct Config {
pub allow_override_action: Option<bool>,
pub allow_remediation_commits: Option<ConfigAllowRemediationCommits>,
pub require: Option<ConfigRequire>,
}

impl Default for Config {
fn default() -> Self {
Self {
allow_override_action: Some(DEFAULT_OVERRIDE_ACTION_ALLOWED),
allow_remediation_commits: Some(ConfigAllowRemediationCommits::default()),
require: Some(ConfigRequire::default()),
}
Expand Down Expand Up @@ -472,6 +475,11 @@ impl Config {
}
}

/// Check if the override action is allowed.
pub fn override_action_is_allowed(&self) -> bool {
self.allow_override_action.unwrap_or(DEFAULT_OVERRIDE_ACTION_ALLOWED)
}

/// Check if the configuration requires members to sign-off commits.
pub fn members_signoff_is_required(&self) -> bool {
if let Some(require) = &self.require {
Expand Down
4 changes: 4 additions & 0 deletions docs/config/dco.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
#
# This configuration file is backwards compatible with the *dcoapp/app* (https://github.com/dcoapp/app) configuration file.

# Show the "Set DCO to pass" button in failed checks
# https://github.com/cncf/dco2?#override-action
allowOverrideAction: true

# https://github.com/cncf/dco2?#remediation-commits
allowRemediationCommits:
# Allow individual remediation commits
Expand Down