Actions: work_item and work_item_comment triggers and the work permission
Work item events start work_item workflows with GitHub-style activity types (opened, edited, closed, reopened, assigned, labeled, unlabeled, milestoned, demilestoned, linked, unlinked, landed), and comments start work_item_comment ones. A job's work permission grants work:read and work:write. GitHub's issues and issue_comment triggers and issues permission still read, start and grant nothing, and say what to write instead.
- G1T-Session:
- ses_01m566c3bte72apbwhjf41ye53
6 files+145−440/6 viewed
| 28 | 28 | "pull.milestoned" => pull("milestoned"), | |
| 29 | 29 | "pull.demilestoned" => pull("demilestoned"), | |
| 30 | 30 | "pull.base_changed" => pull("edited"), | |
| 31 | − | "issue.opened" => vec![("issues", Some("opened"))], | |
| 32 | − | "issue.updated" => vec![("issues", Some("edited"))], | |
| 33 | − | "issue.closed" => vec![("issues", Some("closed"))], | |
| 34 | − | "issue.reopened" => vec![("issues", Some("reopened"))], | |
| 35 | − | "issue.assigned" => vec![("issues", Some("assigned"))], | |
| 36 | − | "issue.labeled" => vec![("issues", Some("labeled"))], | |
| 37 | − | "issue.unlabeled" => vec![("issues", Some("unlabeled"))], | |
| 38 | − | "issue.milestoned" => vec![("issues", Some("milestoned"))], | |
| 39 | − | "issue.demilestoned" => vec![("issues", Some("demilestoned"))], | |
| 40 | − | "comment.created" => vec![("issue_comment", Some("created"))], | |
| 41 | − | "comment.edited" => vec![("issue_comment", Some("edited"))], | |
| 42 | − | "comment.deleted" => vec![("issue_comment", Some("deleted"))], | |
| 31 | + | "work_item.opened" => vec![("work_item", Some("opened"))], | |
| 32 | + | "work_item.updated" => vec![("work_item", Some("edited"))], | |
| 33 | + | "work_item.closed" => vec![("work_item", Some("closed"))], | |
| 34 | + | "work_item.reopened" => vec![("work_item", Some("reopened"))], | |
| 35 | + | "work_item.assigned" => vec![("work_item", Some("assigned"))], | |
| 36 | + | "work_item.labeled" => vec![("work_item", Some("labeled"))], | |
| 37 | + | "work_item.unlabeled" => vec![("work_item", Some("unlabeled"))], | |
| 38 | + | "work_item.milestoned" => vec![("work_item", Some("milestoned"))], | |
| 39 | + | "work_item.demilestoned" => vec![("work_item", Some("demilestoned"))], | |
| 40 | + | "work_item.linked" => vec![("work_item", Some("linked"))], | |
| 41 | + | "work_item.unlinked" => vec![("work_item", Some("unlinked"))], | |
| 42 | + | "work_item.landed" => vec![("work_item", Some("landed"))], | |
| 43 | + | "comment.created" => vec![("work_item_comment", Some("created"))], | |
| 44 | + | "comment.edited" => vec![("work_item_comment", Some("edited"))], | |
| 45 | + | "comment.deleted" => vec![("work_item_comment", Some("deleted"))], | |
| 43 | 46 | "release.created" => vec![("release", Some("created"))], | |
| 44 | 47 | "release.published" => vec![("release", Some("published"))], | |
| 45 | 48 | "release.released" => vec![("release", Some("released"))], | |
| ⋯ | |||
| 308 | 311 | assert_eq!(github_events("git.deleted"), [("delete", None)]); | |
| 309 | 312 | assert_eq!(github_events("pull.labeled")[1], ("pull_request_target", Some("labeled"))); | |
| 310 | 313 | assert_eq!(github_events("pull.assigned")[0], ("pull_request", Some("assigned"))); | |
| 311 | − | assert_eq!(github_events("issue.unlabeled"), [("issues", Some("unlabeled"))]); | |
| 312 | − | assert_eq!(github_events("issue.assigned"), [("issues", Some("assigned"))]); | |
| 314 | + | assert_eq!(github_events("work_item.unlabeled"), [("work_item", Some("unlabeled"))]); | |
| 315 | + | assert_eq!(github_events("work_item.assigned"), [("work_item", Some("assigned"))]); | |
| 316 | + | assert_eq!(github_events("work_item.updated"), [("work_item", Some("edited"))]); | |
| 317 | + | assert_eq!(github_events("work_item.linked"), [("work_item", Some("linked"))]); | |
| 318 | + | assert_eq!(github_events("work_item.landed"), [("work_item", Some("landed"))]); | |
| 319 | + | assert!(github_events("issue.opened").is_empty()); | |
| 313 | 320 | assert_eq!(github_events("pull.updated")[0], ("pull_request", Some("synchronize"))); | |
| 314 | 321 | assert_eq!(github_events("pull.merged")[1], ("pull_request_target", Some("closed"))); | |
| 315 | − | assert_eq!(github_events("comment.created"), [("issue_comment", Some("created"))]); | |
| 316 | − | assert_eq!(github_events("issue.labeled"), [("issues", Some("labeled"))]); | |
| 322 | + | assert_eq!(github_events("comment.created"), [("work_item_comment", Some("created"))]); | |
| 323 | + | assert_eq!(github_events("work_item.labeled"), [("work_item", Some("labeled"))]); | |
| 317 | 324 | assert_eq!(github_events("pull.unlabeled")[0], ("pull_request", Some("unlabeled"))); | |
| 318 | 325 | assert_eq!(github_events("pull.milestoned")[1], ("pull_request_target", Some("milestoned"))); | |
| 319 | 326 | assert_eq!(github_events("pull.base_changed")[0], ("pull_request", Some("edited"))); | |
| 320 | 327 | assert_eq!(github_events("pull.reopened")[0], ("pull_request", Some("reopened"))); | |
| 321 | 328 | assert_eq!(github_events("pull.converted_to_draft")[1], ("pull_request_target", Some("converted_to_draft"))); | |
| 322 | − | assert_eq!(github_events("comment.edited"), [("issue_comment", Some("edited"))]); | |
| 323 | − | assert_eq!(github_events("comment.deleted"), [("issue_comment", Some("deleted"))]); | |
| 329 | + | assert_eq!(github_events("comment.edited"), [("work_item_comment", Some("edited"))]); | |
| 330 | + | assert_eq!(github_events("comment.deleted"), [("work_item_comment", Some("deleted"))]); | |
| 324 | 331 | assert_eq!(github_events("release.published"), [("release", Some("published"))]); | |
| 325 | 332 | assert_eq!(github_events("release.prereleased"), [("release", Some("prereleased"))]); | |
| 326 | 333 | assert_eq!(github_events("deployment_status.created"), [("deployment_status", Some("created"))]); | |
| 1179 | 1179 | "head": { "ref": "feature/x" }, | |
| 1180 | 1180 | "labels": [{ "name": "bug" }, { "name": "Enhancement" }] | |
| 1181 | 1181 | }, | |
| 1182 | − | "issues": [ | |
| 1182 | + | "items": [ | |
| 1183 | 1183 | { "labels": [{ "name": "a" }, { "name": "b" }] }, | |
| 1184 | 1184 | { "labels": [{ "name": "c" }] } | |
| 1185 | 1185 | ], | |
| ⋯ | |||
| 1291 | 1291 | #[test] | |
| 1292 | 1292 | fn nested_object_filters_flatten() { | |
| 1293 | 1293 | assert_eq!( | |
| 1294 | − | ev("github.event.issues.*.labels.*.name"), | |
| 1294 | + | ev("github.event.items.*.labels.*.name"), | |
| 1295 | 1295 | json!(["a", "b", "c"]) | |
| 1296 | 1296 | ); | |
| 1297 | 1297 | assert_eq!( | |
| 13 | 13 | ||
| 14 | 14 | use serde_json::Value; | |
| 15 | 15 | ||
| 16 | − | /// Every permission GitHub's token has, as workflows name them. | |
| 16 | + | /// Every permission a job's token has, as workflows name them: GitHub's, | |
| 17 | + | /// with `work` (work items) in place of its `issues`. | |
| 17 | 18 | pub const NAMES: [&str; 16] = [ | |
| 18 | 19 | "actions", | |
| 19 | 20 | "attestations", | |
| ⋯ | |||
| 22 | 23 | "deployments", | |
| 23 | 24 | "discussions", | |
| 24 | 25 | "id-token", | |
| 25 | − | "issues", | |
| 26 | 26 | "metadata", | |
| 27 | 27 | "models", | |
| 28 | 28 | "packages", | |
| ⋯ | |||
| 31 | 31 | "repository-projects", | |
| 32 | 32 | "security-events", | |
| 33 | 33 | "statuses", | |
| 34 | + | "work", | |
| 34 | 35 | ]; | |
| 35 | 36 | ||
| 36 | 37 | /// Permissions that grant no scope of the token: g1t has nothing behind | |
| ⋯ | |||
| 163 | 164 | }; | |
| 164 | 165 | add("contents", &["code:read"], &["code:write", "repo:write"]); | |
| 165 | 166 | add("pull-requests", &["pull_requests:read"], &["pull_requests:write"]); | |
| 166 | − | add("issues", &["issues:read"], &["issues:write"]); | |
| 167 | + | add("work", &["work:read"], &["work:write"]); | |
| 167 | 168 | add("actions", &["workflows:read"], &["workflows:write"]); | |
| 168 | 169 | add("checks", &["checks:read"], &["checks:write"]); | |
| 169 | 170 | add("statuses", &["checks:read"], &["checks:write"]); | |
| ⋯ | |||
| 220 | 221 | let restricted = Permissions::default_for(TokenDefault::Restricted); | |
| 221 | 222 | assert_eq!(restricted.get("contents"), Access::Read); | |
| 222 | 223 | assert_eq!(restricted.get("packages"), Access::Read); | |
| 223 | − | assert_eq!(restricted.get("issues"), Access::None); | |
| 224 | + | assert_eq!(restricted.get("work"), Access::None); | |
| 224 | 225 | assert_eq!(restricted.get("metadata"), Access::Read); | |
| 225 | 226 | assert_eq!(restricted.scopes(), ["repo:read", "code:read", "packages:read"]); | |
| 226 | 227 | let permissive = Permissions::default_for(TokenDefault::Permissive); | |
| ⋯ | |||
| 232 | 233 | ||
| 233 | 234 | #[test] | |
| 234 | 235 | fn written_permissions_leave_the_rest_at_none() { | |
| 235 | − | let (permissions, unknown) = parse(&json!({ "contents": "write", "pull-requests": "write", "issues": "read" })).unwrap(); | |
| 236 | + | let (permissions, unknown) = parse(&json!({ "contents": "write", "pull-requests": "write", "work": "read" })).unwrap(); | |
| 236 | 237 | assert!(unknown.is_empty()); | |
| 237 | 238 | assert_eq!( | |
| 238 | 239 | permissions.scopes(), | |
| 239 | − | ["repo:read", "code:read", "code:write", "repo:write", "pull_requests:read", "pull_requests:write", "issues:read"] | |
| 240 | + | ["repo:read", "code:read", "code:write", "repo:write", "pull_requests:read", "pull_requests:write", "work:read"] | |
| 240 | 241 | ); | |
| 241 | 242 | assert_eq!(permissions.get("packages"), Access::None); | |
| 242 | 243 | // `{}` is nothing but metadata. | |
| ⋯ | |||
| 249 | 250 | let (all, _) = parse(&json!("write-all")).unwrap(); | |
| 250 | 251 | let scopes = all.scopes(); | |
| 251 | 252 | for scope in [ | |
| 252 | − | "workflows:write", "checks:write", "deployments:write", "packages:write", "security:write", "issues:write", | |
| 253 | + | "workflows:write", "checks:write", "deployments:write", "packages:write", "security:write", "work:write", | |
| 253 | 254 | ] { | |
| 254 | 255 | assert!(scopes.contains(&scope), "{scope}"); | |
| 255 | 256 | } | |
| ⋯ | |||
| 278 | 279 | #[test] | |
| 279 | 280 | fn a_called_workflow_gets_no_more_than_its_caller() { | |
| 280 | 281 | let (callee, _) = parse(&json!("write-all")).unwrap(); | |
| 281 | − | let (caller, _) = parse(&json!({ "contents": "write", "issues": "read" })).unwrap(); | |
| 282 | + | let (caller, _) = parse(&json!({ "contents": "write", "work": "read" })).unwrap(); | |
| 282 | 283 | let capped = callee.capped_by(&caller); | |
| 283 | 284 | assert_eq!(capped.get("contents"), Access::Write); | |
| 284 | − | assert_eq!(capped.get("issues"), Access::Read); | |
| 285 | + | assert_eq!(capped.get("work"), Access::Read); | |
| 285 | 286 | assert_eq!(capped.get("pull-requests"), Access::None); | |
| 286 | 287 | } | |
| 287 | 288 | ||
| ⋯ | |||
| 292 | 293 | assert!(parse(&json!(["contents"])).is_err()); | |
| 293 | 294 | let (_, unknown) = parse(&json!({ "contents": "read", "wiki": "write" })).unwrap(); | |
| 294 | 295 | assert_eq!(unknown, ["wiki"]); | |
| 296 | + | // GitHub's `issues` is `work` on g1t: alone it grants nothing. | |
| 297 | + | let (issues, unknown) = parse(&json!({ "issues": "write" })).unwrap(); | |
| 298 | + | assert_eq!(unknown, ["issues"]); | |
| 299 | + | assert_eq!(issues.scopes(), ["repo:read"]); | |
| 300 | + | let (work, _) = parse(&json!({ "work": "write" })).unwrap(); | |
| 301 | + | assert_eq!(work.scopes(), ["repo:read", "work:read", "work:write"]); | |
| 295 | 302 | } | |
| 296 | 303 | } | |
| 1 | 1 | //! GitHub's workflow syntax: the keys each mapping in a workflow may have, | |
| 2 | 2 | //! as GitHub's own schema lists them (`schema/workflow-v1.0.json`, from | |
| 3 | 3 | //! actions/languageservices; the tests below check these lists against | |
| 4 | − | //! it). [`crate::workflow::parse`] refuses any other key, as GitHub does, | |
| 5 | − | //! except `g1t`, g1t's own settings, on the workflow, a job and a step. | |
| 4 | + | //! it), with g1t's own beside them: the `work_item` and | |
| 5 | + | //! `work_item_comment` events ([`OWN_EVENTS`]) and the `work` permission | |
| 6 | + | //! ([`OWN_PERMISSIONS`]). [`crate::workflow::parse`] refuses any other | |
| 7 | + | //! key, as GitHub does, except `g1t`, g1t's own settings, on the workflow, | |
| 8 | + | //! a job and a step. | |
| 6 | 9 | ||
| 7 | 10 | /// g1t's own key, allowed on the workflow, each job and each step. | |
| 8 | 11 | pub const EXTENSION: &str = "g1t"; | |
| ⋯ | |||
| 74 | 77 | /// `defaults.run` (`workflow-defaults-run`, `job-defaults-run`). | |
| 75 | 78 | pub const DEFAULTS_RUN: &[&str] = &["shell", "working-directory"]; | |
| 76 | 79 | ||
| 77 | − | /// `permissions` as a mapping (`permissions-mapping`). | |
| 80 | + | /// `permissions` as a mapping (`permissions-mapping`), and g1t's own. | |
| 78 | 81 | pub const PERMISSIONS: &[&str] = &[ | |
| 79 | 82 | "actions", | |
| 80 | 83 | "artifact-metadata", | |
| ⋯ | |||
| 96 | 99 | "security-events", | |
| 97 | 100 | "statuses", | |
| 98 | 101 | "vulnerability-alerts", | |
| 102 | + | "work", | |
| 99 | 103 | ]; | |
| 100 | 104 | ||
| 105 | + | /// Permissions g1t has that GitHub does not: `work`, for work items. | |
| 106 | + | pub const OWN_PERMISSIONS: &[&str] = &["work"]; | |
| 107 | + | ||
| 101 | 108 | /// Each entry of `on.schedule` (`cron-mapping`). | |
| 102 | 109 | pub const CRON: &[&str] = &["cron", "timezone"]; | |
| 103 | 110 | ||
| ⋯ | |||
| 133 | 140 | const TYPES: EventSpec = EventSpec::Filters(&["types"]); | |
| 134 | 141 | const PULL_REQUEST: EventSpec = EventSpec::Filters(&["types", "branches", "branches-ignore", "paths", "paths-ignore"]); | |
| 135 | 142 | ||
| 136 | − | /// Every event GitHub has, and what each takes (`on-mapping-strict`). | |
| 143 | + | /// Every event GitHub has, and what each takes (`on-mapping-strict`), | |
| 144 | + | /// and g1t's own. | |
| 137 | 145 | pub const EVENTS: &[(&str, EventSpec)] = &[ | |
| 138 | 146 | ("branch_protection_rule", TYPES), | |
| 139 | 147 | ("check_run", TYPES), | |
| ⋯ | |||
| 172 | 180 | ("workflow_call", EventSpec::Filters(WORKFLOW_CALL)), | |
| 173 | 181 | ("workflow_dispatch", EventSpec::Filters(WORKFLOW_DISPATCH)), | |
| 174 | 182 | ("workflow_run", EventSpec::Filters(&["types", "branches", "branches-ignore", "workflows"])), | |
| 183 | + | ("work_item", TYPES), | |
| 184 | + | ("work_item_comment", TYPES), | |
| 175 | 185 | ]; | |
| 176 | 186 | ||
| 187 | + | /// Events g1t has that GitHub does not: a work item changed, and a comment | |
| 188 | + | /// on a work item or a pull request. | |
| 189 | + | pub const OWN_EVENTS: &[&str] = &["work_item", "work_item_comment"]; | |
| 190 | + | ||
| 177 | 191 | /// What `event` takes, if GitHub has it. | |
| 178 | 192 | pub fn event(event: &str) -> Option<EventSpec> { | |
| 179 | 193 | EVENTS.iter().find(|(name, _)| *name == event).map(|(_, spec)| *spec) | |
| ⋯ | |||
| 212 | 226 | list.iter().map(|key| (*key).to_owned()).collect() | |
| 213 | 227 | } | |
| 214 | 228 | ||
| 229 | + | /// GitHub's part of a list: without g1t's own. | |
| 230 | + | fn upstream(list: &[&str], own: &[&str]) -> BTreeSet<String> { | |
| 231 | + | list.iter().filter(|key| !own.contains(key)).map(|key| (*key).to_owned()).collect() | |
| 232 | + | } | |
| 233 | + | ||
| 215 | 234 | #[test] | |
| 216 | 235 | fn the_lists_are_githubs() { | |
| 217 | 236 | let schema = schema(); | |
| ⋯ | |||
| 241 | 260 | (CALL_OUTPUT, "workflow-call-output-definition"), | |
| 242 | 261 | (CALL_SECRET, "workflow-call-secret-mapping-definition"), | |
| 243 | 262 | ] { | |
| 244 | − | assert_eq!(set(list), keys(&schema, name), "{name}"); | |
| 263 | + | assert_eq!(upstream(list, OWN_PERMISSIONS), keys(&schema, name), "{name}"); | |
| 245 | 264 | } | |
| 265 | + | for own in OWN_PERMISSIONS { | |
| 266 | + | assert!(PERMISSIONS.contains(own) && !keys(&schema, "permissions-mapping").contains(*own), "{own}"); | |
| 267 | + | } | |
| 246 | 268 | // The background steps' own keys. | |
| 247 | 269 | let mut background = BTreeSet::from(["background".to_owned()]); | |
| 248 | 270 | for name in ["wait-step", "wait-all-step", "cancel-step", "parallel-step"] { | |
| ⋯ | |||
| 261 | 283 | let schema = schema(); | |
| 262 | 284 | let definitions = &schema["definitions"]; | |
| 263 | 285 | let events = definitions["on-mapping-strict"]["mapping"]["properties"].as_object().unwrap(); | |
| 264 | − | assert_eq!(events.keys().cloned().collect::<BTreeSet<_>>(), EVENTS.iter().map(|(name, _)| (*name).to_owned()).collect()); | |
| 286 | + | let names: Vec<&str> = EVENTS.iter().map(|(name, _)| *name).collect(); | |
| 287 | + | assert_eq!(events.keys().cloned().collect::<BTreeSet<_>>(), upstream(&names, OWN_EVENTS)); | |
| 288 | + | for own in OWN_EVENTS { | |
| 289 | + | assert!(!events.contains_key(*own), "{own}"); | |
| 290 | + | assert_eq!(event(own), Some(TYPES), "{own}"); | |
| 291 | + | } | |
| 265 | 292 | for (name, kind) in events { | |
| 266 | 293 | let kind = kind.as_str().or_else(|| kind["type"].as_str()).unwrap(); | |
| 267 | 294 | let definition = &definitions[kind]; | |
| 20 | 20 | "pull_request", | |
| 21 | 21 | "pull_request_target", | |
| 22 | 22 | "pull_request_review", | |
| 23 | − | "issues", | |
| 24 | − | "issue_comment", | |
| 23 | + | "work_item", | |
| 24 | + | "work_item_comment", | |
| 25 | 25 | "schedule", | |
| 26 | 26 | "workflow_dispatch", | |
| 27 | 27 | "repository_dispatch", | |
| ⋯ | |||
| 37 | 37 | ||
| 38 | 38 | /// Events GitHub has that g1t knows of but never sends: a workflow on one | |
| 39 | 39 | /// of them is told so, rather than waiting for a run that never comes. | |
| 40 | − | pub const UNSENT_EVENTS: &[(&str, &str)] = &[( | |
| 41 | − | "label", | |
| 42 | − | "g1t does not start runs when a label is created, edited or deleted yet, so the `label` trigger never starts it. Putting a label on an issue or a pull request starts `issues` and `pull_request` workflows with the `labeled` type.", | |
| 43 | − | )]; | |
| 40 | + | pub const UNSENT_EVENTS: &[(&str, &str)] = &[ | |
| 41 | + | ( | |
| 42 | + | "label", | |
| 43 | + | "g1t does not start runs when a label is created, edited or deleted yet, so the `label` trigger never starts it. Putting a label on a work item or a pull request starts `work_item` and `pull_request` workflows with the `labeled` type.", | |
| 44 | + | ), | |
| 45 | + | ( | |
| 46 | + | "issues", | |
| 47 | + | "g1t has work items, not issues, so the `issues` trigger never starts it. Write `work_item`: it has the same activity types, and `github.event.work_item` in place of `github.event.issue`.", | |
| 48 | + | ), | |
| 49 | + | ( | |
| 50 | + | "issue_comment", | |
| 51 | + | "g1t has work items, not issues, so the `issue_comment` trigger never starts it. Write `work_item_comment`: it runs for comments on work items and pull requests, with `github.event.work_item` in place of `github.event.issue`.", | |
| 52 | + | ), | |
| 53 | + | ]; | |
| 44 | 54 | ||
| 45 | 55 | /// The `types` each event has when a workflow gives none, as on GitHub. | |
| 46 | 56 | pub fn default_types(event: &str) -> &'static [&'static str] { | |
| ⋯ | |||
| 864 | 874 | /// nothing behind. | |
| 865 | 875 | fn permission_notes(unknown: &[String], job: Option<&str>, note: &mut impl FnMut(Severity, Option<&str>, String)) { | |
| 866 | 876 | for name in unknown { | |
| 867 | − | note(Severity::Warning, job, format!("`permissions.{name}`: g1t's token has nothing behind that permission yet, so it grants nothing.")); | |
| 877 | + | let message = if name == "issues" { | |
| 878 | + | "`permissions.issues`: g1t has work items, not issues, so it grants nothing. Write `work` for the token to read or change work items.".to_owned() | |
| 879 | + | } else { | |
| 880 | + | format!("`permissions.{name}`: g1t's token has nothing behind that permission yet, so it grants nothing.") | |
| 881 | + | }; | |
| 882 | + | note(Severity::Warning, job, message); | |
| 868 | 883 | } | |
| 869 | 884 | } | |
| 870 | 885 | ||
| ⋯ | |||
| 1069 | 1084 | // Without any, the repository's default. | |
| 1070 | 1085 | let bare = parse("on: push\njobs:\n a:\n runs-on: ubuntu-latest\n steps: [{ run: 'true' }]").unwrap(); | |
| 1071 | 1086 | assert_eq!(bare.jobs[0].permissions(&bare, TokenDefault::Restricted).get("contents"), Access::Read); | |
| 1072 | − | assert_eq!(bare.jobs[0].permissions(&bare, TokenDefault::Restricted).get("issues"), Access::None); | |
| 1073 | − | assert_eq!(bare.jobs[0].permissions(&bare, TokenDefault::Permissive).get("issues"), Access::Write); | |
| 1087 | + | assert_eq!(bare.jobs[0].permissions(&bare, TokenDefault::Restricted).get("work"), Access::None); | |
| 1088 | + | assert_eq!(bare.jobs[0].permissions(&bare, TokenDefault::Permissive).get("work"), Access::Write); | |
| 1074 | 1089 | // What reads but grants nothing is said. | |
| 1075 | 1090 | let odd = parse("on: push\npermissions: { id-token: write, artifact-metadata: read }\njobs:\n a:\n runs-on: x\n steps: [{ run: 'true' }]").unwrap(); | |
| 1076 | 1091 | assert!(!odd.notes.iter().any(|n| n.message.contains("id-token")), "OIDC tokens are issued"); | |
| ⋯ | |||
| 1178 | 1193 | } | |
| 1179 | 1194 | ||
| 1180 | 1195 | #[test] | |
| 1196 | + | fn work_items_start_workflows_and_issues_do_not() { | |
| 1197 | + | use crate::permissions::{Access, TokenDefault}; | |
| 1198 | + | let workflow = parse( | |
| 1199 | + | "on: | |
| 1200 | + | work_item: { types: [opened, landed] } | |
| 1201 | + | work_item_comment: | |
| 1202 | + | permissions: { work: write } | |
| 1203 | + | jobs: | |
| 1204 | + | a: | |
| 1205 | + | runs-on: x | |
| 1206 | + | steps: [{ run: 'true' }]", | |
| 1207 | + | ) | |
| 1208 | + | .unwrap(); | |
| 1209 | + | assert!(workflow.notes.is_empty(), "{:?}", workflow.notes); | |
| 1210 | + | let item = workflow.trigger("work_item").unwrap(); | |
| 1211 | + | assert!(item.wants_type(Some("landed"))); | |
| 1212 | + | assert!(!item.wants_type(Some("closed"))); | |
| 1213 | + | assert!(workflow.trigger("work_item_comment").unwrap().wants_type(Some("created"))); | |
| 1214 | + | assert_eq!(workflow.jobs[0].permissions(&workflow, TokenDefault::Restricted).get("work"), Access::Write); | |
| 1215 | + | // GitHub's names still read, as GitHub workflows do, but start | |
| 1216 | + | // nothing and grant nothing, and say what to write instead. | |
| 1217 | + | let github = parse("on: [issues, issue_comment]\npermissions: { issues: write }\njobs:\n a:\n runs-on: x\n steps: [{ run: 'true' }]").unwrap(); | |
| 1218 | + | assert!(github.trigger("work_item").is_none()); | |
| 1219 | + | let said: Vec<&str> = github.notes.iter().map(|note| note.message.as_str()).collect(); | |
| 1220 | + | assert!(said.iter().any(|note| note.contains("Write `work_item`")), "{said:?}"); | |
| 1221 | + | assert!(said.iter().any(|note| note.contains("Write `work_item_comment`")), "{said:?}"); | |
| 1222 | + | assert!(said.iter().any(|note| note.contains("Write `work`")), "{said:?}"); | |
| 1223 | + | assert_eq!(github.jobs[0].permissions(&github, TokenDefault::Restricted).scopes(), ["repo:read"]); | |
| 1224 | + | } | |
| 1225 | + | ||
| 1226 | + | #[test] | |
| 1181 | 1227 | fn aliases_read_and_merge_keys_do_not() { | |
| 1182 | 1228 | let aliased = parse("on: push\njobs:\n a:\n runs-on: x\n steps: &steps\n - run: 'true'\n b:\n runs-on: x\n steps: *steps").unwrap(); | |
| 1183 | 1229 | assert_eq!(aliased.jobs[1].steps.len(), 1); | |
| 1 | + | name: Triage | |
| 2 | + | on: | |
| 3 | + | work_item: | |
| 4 | + | types: [opened, labeled, landed] | |
| 5 | + | work_item_comment: | |
| 6 | + | types: [created] | |
| 7 | + | permissions: | |
| 8 | + | work: write | |
| 9 | + | jobs: | |
| 10 | + | triage: | |
| 11 | + | if: ${{ contains(github.event.work_item.labels.*.name, 'bug') && !github.event.work_item.pull_request }} | |
| 12 | + | runs-on: ubuntu-latest | |
| 13 | + | steps: | |
| 14 | + | - run: echo "${{ github.event.work_item.html_url }} in ${{ github.event.work_item.workspace }}" |
Comments
Sign in to comment.