ci: Set and enforce more default permissions (#60222)

Release Notes:

- N/A
This commit is contained in:
Finn Evers 2026-07-06 18:02:54 +02:00 committed by GitHub
parent 5d6c88cdb7
commit 283d5054ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 388 additions and 136 deletions

View file

@ -22,6 +22,8 @@ on:
description: body
type: string
default: ''
permissions:
contents: read
jobs:
rebuild_releases_page:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -13,9 +13,14 @@ on:
description: run_clippy
type: boolean
default: 'true'
permissions:
contents: read
jobs:
run_autofix:
runs-on: namespace-profile-16x32-ubuntu-2204
permissions:
contents: read
pull-requests: read
env:
CC: clang
CXX: clang++

View file

@ -5,10 +5,15 @@ on:
# Fire every day at 16:00 UTC (At the start of the US workday)
- cron: "0 16 * * *"
permissions:
contents: read
jobs:
update-collab-staging-tag:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1

View file

@ -8,10 +8,14 @@ on:
description: Branch name to run on
required: true
type: string
permissions:
contents: read
jobs:
run_bump_patch_version:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-16x32-ubuntu-2204
permissions:
contents: write
steps:
- id: generate-token
name: steps::authenticate_as_zippy

View file

@ -8,6 +8,8 @@ on:
description: 'Which channels to bump: all, main, preview, or stable'
type: string
default: all
permissions:
contents: read
jobs:
resolve_versions:
if: github.repository_owner == 'zed-industries'
@ -86,6 +88,9 @@ jobs:
- resolve_versions
if: inputs.target == 'all' || inputs.target == 'main'
runs-on: namespace-profile-16x32-ubuntu-2204
permissions:
contents: write
pull-requests: write
steps:
- id: generate-token
name: steps::authenticate_as_zippy
@ -127,6 +132,8 @@ jobs:
- resolve_versions
if: inputs.target == 'all' || inputs.target == 'preview'
runs-on: namespace-profile-16x32-ubuntu-2204
permissions:
contents: write
steps:
- id: generate-token
name: steps::authenticate_as_zippy
@ -180,6 +187,8 @@ jobs:
- resolve_versions
if: inputs.target == 'all' || inputs.target == 'stable'
runs-on: namespace-profile-16x32-ubuntu-2204
permissions:
contents: write
steps:
- id: generate-token
name: steps::authenticate_as_zippy

View file

@ -21,6 +21,8 @@ on:
description: pr_number
required: true
type: string
permissions:
contents: read
jobs:
run_cherry_pick:
runs-on: namespace-profile-2x4-ubuntu-2404

View file

@ -14,6 +14,8 @@ concurrency:
group: potential-duplicate-check-${{ github.event.issue.number || inputs.issue_number }}
cancel-in-progress: true
permissions: {}
jobs:
identify-duplicates:
# For manual testing, allow running on any branch; for automatic runs, only on main repo

View file

@ -13,10 +13,15 @@ on:
type: number
default: 1000
permissions:
contents: read
jobs:
stale:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
permissions:
issues: write
steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
with:

View file

@ -5,10 +5,16 @@ on:
- cron: "0 */12 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
update_top_ranking_issues:
runs-on: namespace-profile-2x4-ubuntu-2404
if: github.repository == 'zed-industries/zed'
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Set up uv

View file

@ -5,10 +5,16 @@ on:
- cron: "0 15 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
update_top_ranking_issues:
runs-on: namespace-profile-2x4-ubuntu-2404
if: github.repository == 'zed-industries/zed'
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Set up uv

View file

@ -7,6 +7,8 @@ on:
schedule:
- cron: 30 17 * * 2
workflow_dispatch: {}
permissions:
contents: read
jobs:
scheduled_compliance_check:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -4,6 +4,9 @@ on:
push:
branches: [main]
permissions:
contents: read
jobs:
check-author:
if: ${{ github.repository_owner == 'zed-industries' }}

View file

@ -11,6 +11,8 @@ on:
- edited
branches:
- main
permissions:
contents: read
jobs:
danger:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -7,6 +7,8 @@ on:
push:
tags:
- collab-production
permissions:
contents: read
jobs:
style:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -35,6 +35,8 @@ on:
description: Git ref to checkout and deploy. Defaults to event SHA when omitted.
type: string
default: ''
permissions:
contents: read
jobs:
deploy_docs:
if: github.repository_owner == 'zed-industries'

View file

@ -5,6 +5,7 @@ on:
push:
branches:
- main
permissions: {}
jobs:
deploy_docs:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -42,6 +42,10 @@ on:
- immediate
default: batch
# Both jobs below declare their own `permissions:` blocks, so no job uses this
# top-level default. Least privilege therefore grants nothing here.
permissions: {}
env:
DROID_MODEL: claude-sonnet-4-5-20250929
SUGGESTIONS_BRANCH: docs/suggestions-pending

View file

@ -10,6 +10,8 @@ on:
- '!extensions/test-extension/**'
- '!extensions/workflows/**'
- '!extensions/*.md'
permissions:
contents: read
jobs:
detect_changed_extensions:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -28,6 +28,8 @@ on:
app-secret:
description: The app secret for the corresponding app ID
required: true
permissions:
contents: read
jobs:
check_version_changed:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -15,6 +15,8 @@ on:
description: working-directory
type: string
default: .
permissions:
contents: read
jobs:
orchestrate:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -14,6 +14,8 @@ on:
description: Description for the changes to be expected with this rollout
type: string
default: ''
permissions:
contents: read
jobs:
fetch_extension_repos:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && (github.ref == 'refs/tags/extension-workflows' || github.ref == 'refs/heads/main')

View file

@ -4,6 +4,9 @@ on:
issues:
types: [labeled]
permissions:
contents: read
jobs:
handle-good-first-issue:
if: github.event.label.name == '.contrib/good first issue' && github.repository_owner == 'zed-industries'

View file

@ -21,12 +21,13 @@ on:
permissions:
contents: read
pull-requests: read
jobs:
check-hotfix-reviews:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
permissions:
pull-requests: read
timeout-minutes: 5
env:
REPO: ${{ github.repository }}

View file

@ -9,6 +9,8 @@ on:
types:
- labeled
- synchronize
permissions:
contents: read
jobs:
build_nix_linux_x86_64:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && ((github.event.action == 'labeled' && (github.event.label.name == 'run-nix' || github.event.label.name == 'run-bundling')) || (github.event.action == 'synchronize' && (contains(github.event.pull_request.labels.*.name, 'run-nix') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))))

View file

@ -11,6 +11,8 @@ on:
description: Describe why the extension CLI is being bumped and/or what changes are included.
required: true
type: string
permissions:
contents: read
jobs:
publish_job:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.ref == 'refs/heads/main'

View file

@ -8,6 +8,8 @@ on:
push:
tags:
- v*
permissions:
contents: read
jobs:
run_tests_mac:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@ -723,6 +725,8 @@ jobs:
- bundle_windows_aarch64
- bundle_windows_x86_64
runs-on: namespace-profile-4x8-ubuntu-2204
permissions:
contents: write
steps:
- name: release::download_workflow_artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c

View file

@ -8,6 +8,8 @@ on:
schedule:
- cron: 0 */4 * * *
workflow_dispatch: {}
permissions:
contents: read
jobs:
check_nightly_tag:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -9,6 +9,8 @@ on:
types:
- labeled
- synchronize
permissions:
contents: read
jobs:
bundle_linux_aarch64:
if: |-

View file

@ -14,6 +14,8 @@ on:
branches:
- main
- v[0-9]+.[0-9]+.x
permissions:
contents: read
jobs:
orchestrate:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -18,6 +18,9 @@ on:
- "Guild New PR Notification"
types: [completed]
permissions:
contents: read
jobs:
notify-slack:
if: >-

View file

@ -4,6 +4,9 @@ on:
issues:
types: [labeled]
permissions:
contents: read
env:
PRIORITY_LABELS: '["priority:P0", "priority:P1"]'
REPRODUCIBLE_LABEL: 'state:reproducible'

View file

@ -4,6 +4,9 @@ on:
label:
types: [created]
permissions:
contents: read
jobs:
notify-slack:
if: >-

View file

@ -20,13 +20,14 @@ on:
permissions:
contents: read
pull-requests: read
jobs:
check-stale-prs:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: read
env:
REPO: ${{ github.repository }}
# Only surface PRs created on or after this date. Update this if the

View file

@ -5,10 +5,16 @@ on:
- cron: "0 6 * * 1,4" # Mondays and Thursdays at 6 AM UTC
workflow_dispatch:
permissions:
contents: read
jobs:
update-duplicate-magnets:
runs-on: ubuntu-latest
if: github.repository == 'zed-industries/zed'
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1

View file

@ -8,6 +8,8 @@ on:
push:
branches:
- main
permissions:
contents: read
jobs:
call_extension_tests:
permissions:

View file

@ -11,6 +11,7 @@ on:
paths-ignore:
- .github/**
workflow_dispatch: {}
permissions: {}
jobs:
determine_bump_type:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -1,20 +1,22 @@
mod check_permissions;
mod check_run_patterns;
use std::{fs, path::PathBuf};
use std::{
fs,
path::{Path, PathBuf},
};
use annotate_snippets::Renderer;
use annotate_snippets::{Group, Renderer};
use anyhow::{Result, anyhow};
use clap::Parser;
use itertools::{Either, Itertools};
use serde_yaml::Value;
use strum::IntoEnumIterator;
use crate::tasks::{
workflow_checks::check_run_patterns::{
RunValidationError, WorkflowFile, WorkflowValidationError,
},
workflows::WorkflowType,
};
use crate::tasks::workflows::WorkflowType;
use check_permissions::PermissionsError;
use check_run_patterns::RunValidationError;
pub use check_run_patterns::validate_run_command;
@ -36,14 +38,14 @@ pub fn validate(_: WorkflowValidationArgs) -> Result<()> {
parsing_errors.into_iter().join("\n")
))
} else if !file_errors.is_empty() {
let errors: Vec<_> = file_errors
let groups: Vec<_> = file_errors
.iter()
.map(|error| error.annotation_group())
.flat_map(|error| error.annotation_groups())
.collect();
let renderer =
Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Ascii);
println!("{}", renderer.render(errors.as_slice()));
println!("{}", renderer.render(groups.as_slice()));
Err(anyhow!("Workflow checks failed!"))
} else {
@ -51,6 +53,61 @@ pub fn validate(_: WorkflowValidationArgs) -> Result<()> {
}
}
struct WorkflowFile {
raw_content: String,
parsed_content: Value,
}
impl WorkflowFile {
fn load(workflow_file_path: &Path) -> Result<Self> {
fs::read_to_string(workflow_file_path)
.map_err(|_| {
anyhow!(
"Could not read workflow file at {}",
workflow_file_path.display()
)
})
.and_then(|file_content| {
serde_yaml::from_str(&file_content)
.map(|parsed_content| Self {
raw_content: file_content,
parsed_content,
})
.map_err(|e| anyhow!("Failed to parse workflow file: {e:?}"))
})
}
}
/// A single kind of validation failure found within a workflow file.
enum ValidationError {
RunInjection(RunValidationError),
Permissions(PermissionsError),
}
impl ValidationError {
fn annotation_group<'a>(&self, file_path: &Path, raw_content: &'a str) -> Group<'a> {
match self {
ValidationError::RunInjection(error) => error.annotation_group(file_path, raw_content),
ValidationError::Permissions(error) => error.annotation_group(file_path, raw_content),
}
}
}
struct WorkflowValidationError {
file_path: PathBuf,
contents: WorkflowFile,
errors: Vec<ValidationError>,
}
impl WorkflowValidationError {
fn annotation_groups(&self) -> Vec<Group<'_>> {
self.errors
.iter()
.map(|error| error.annotation_group(&self.file_path, &self.contents.raw_content))
.collect()
}
}
enum WorkflowError {
ParseError(anyhow::Error),
ValidationError(Box<WorkflowValidationError>),
@ -73,41 +130,47 @@ fn get_all_workflow_files() -> impl Iterator<Item = PathBuf> {
}
fn check_workflow(workflow_file_path: PathBuf) -> Result<(), WorkflowError> {
fn collect_errors(
iter: impl Iterator<Item = Result<(), Vec<RunValidationError>>>,
) -> Result<(), Vec<RunValidationError>> {
Some(iter.flat_map(Result::err).flatten().collect::<Vec<_>>())
.filter(|errors| !errors.is_empty())
.map_or(Ok(()), Err)
}
fn check_recursive(key: &Value, value: &Value) -> Result<(), Vec<RunValidationError>> {
match value {
Value::Mapping(mapping) => collect_errors(
mapping
.into_iter()
.map(|(key, value)| check_recursive(key, value)),
),
Value::Sequence(sequence) => collect_errors(
sequence
.into_iter()
.map(|value| check_recursive(key, value)),
),
Value::String(string) => check_string(key, string).map_err(|error| vec![error]),
Value::Null | Value::Bool(_) | Value::Number(_) | Value::Tagged(_) => Ok(()),
}
}
let file_content =
WorkflowFile::load(&workflow_file_path).map_err(WorkflowError::ParseError)?;
check_recursive(&Value::Null, &file_content.parsed_content).map_err(|errors| {
WorkflowError::ValidationError(Box::new(WorkflowValidationError::new(
errors,
file_content,
workflow_file_path,
let mut errors = Vec::new();
if let Err(error) = check_permissions::validate_permissions(&file_content.parsed_content) {
errors.push(ValidationError::Permissions(error));
}
errors.extend(
collect_run_injection_errors(&Value::Null, &file_content.parsed_content)
.into_iter()
.map(ValidationError::RunInjection),
);
if errors.is_empty() {
Ok(())
} else {
Err(WorkflowError::ValidationError(Box::new(
WorkflowValidationError {
file_path: workflow_file_path,
contents: file_content,
errors,
},
)))
})
}
}
fn collect_run_injection_errors(key: &Value, value: &Value) -> Vec<RunValidationError> {
match value {
Value::Mapping(mapping) => mapping
.iter()
.flat_map(|(key, value)| collect_run_injection_errors(key, value))
.collect(),
Value::Sequence(sequence) => sequence
.iter()
.flat_map(|value| collect_run_injection_errors(key, value))
.collect(),
Value::String(string) => check_string(key, string).err().into_iter().collect(),
Value::Null | Value::Bool(_) | Value::Number(_) | Value::Tagged(_) => Vec::new(),
}
}
fn check_string(key: &Value, value: &str) -> Result<(), RunValidationError> {

View file

@ -0,0 +1,74 @@
use annotate_snippets::{AnnotationKind, Group, Level, Snippet};
use serde_yaml::Value;
use std::{ops::Range, path::Path};
pub enum PermissionsError {
/// No top-level `permissions:` key.
Missing,
/// Top-level default grants more than `contents: read`.
ExcessiveDefault,
}
/// Validates a workflow's top-level (default) `permissions:`.
///
/// Every workflow must declare one, and it may grant at most `contents: read`;
/// jobs that need more must request it at the job level.
pub fn validate_permissions(workflow: &Value) -> Result<(), PermissionsError> {
let Some(permissions) = workflow.get("permissions") else {
return Err(PermissionsError::Missing);
};
let is_minimal = match permissions {
Value::Mapping(mapping) => mapping.iter().all(|(scope, level)| {
let level = level.as_str();
level == Some("none") || (scope.as_str() == Some("contents") && level == Some("read"))
}),
// String forms such as `read-all`/`write-all` always exceed the allowance.
_ => false,
};
if is_minimal {
Ok(())
} else {
Err(PermissionsError::ExcessiveDefault)
}
}
impl PermissionsError {
pub fn annotation_group<'a>(&self, file_path: &Path, raw_content: &'a str) -> Group<'a> {
let (title, span, label) = match self {
PermissionsError::Missing => (
"Workflow is missing a top-level `permissions:` key",
first_line_span(raw_content, "name:"),
"Add a top-level `permissions:` key so this workflow defaults to the least privilege it needs",
),
PermissionsError::ExcessiveDefault => (
"Top-level workflow permissions must grant at most `contents: read`",
first_line_span(raw_content, "permissions:"),
"Lower the default to `contents: read` (or `{}`) and move elevated permissions to the jobs that need them",
),
};
Level::ERROR.primary_title(title).element(
Snippet::source(raw_content)
.path(file_path.display().to_string())
.annotations(std::iter::once(
AnnotationKind::Primary.span(span).label(label),
)),
)
}
}
/// Span of the first column-0 line starting with `prefix`, else empty.
fn first_line_span(raw_content: &str, prefix: &str) -> Range<usize> {
let mut offset = 0;
for line in raw_content.lines() {
let start = offset;
offset += line.len() + 1;
if line.starts_with(prefix) {
return start..start + line.len();
}
}
Default::default()
}

View file

@ -1,72 +1,25 @@
use annotate_snippets::{AnnotationKind, Group, Level, Snippet};
use anyhow::{Result, anyhow};
use regex::Regex;
use serde_yaml::Value;
use std::{
collections::HashMap,
fs,
ops::Range,
path::{Path, PathBuf},
sync::LazyLock,
};
use std::{collections::HashMap, ops::Range, path::Path, sync::LazyLock};
static GITHUB_INPUT_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"\$\{\{[[:blank:]]*([[:alnum:]]|[[:punct:]])+?[[:blank:]]*\}\}"#)
.expect("Should compile")
});
pub struct WorkflowFile {
raw_content: String,
pub parsed_content: Value,
pub struct RunValidationError {
found_injection_patterns: Vec<(String, Range<usize>)>,
}
impl WorkflowFile {
pub fn load(workflow_file_path: &Path) -> Result<Self> {
fs::read_to_string(workflow_file_path)
.map_err(|_| {
anyhow!(
"Could not read workflow file at {}",
workflow_file_path.display()
)
})
.and_then(|file_content| {
serde_yaml::from_str(&file_content)
.map(|parsed_content| Self {
raw_content: file_content,
parsed_content,
})
.map_err(|e| anyhow!("Failed to parse workflow file: {e:?}"))
})
}
}
pub struct WorkflowValidationError {
file_path: PathBuf,
contents: WorkflowFile,
errors: Vec<RunValidationError>,
}
impl WorkflowValidationError {
pub fn new(
errors: Vec<RunValidationError>,
contents: WorkflowFile,
file_path: PathBuf,
) -> Self {
Self {
file_path,
contents,
errors,
}
}
pub fn annotation_group<'a>(&'a self) -> Group<'a> {
let raw_content = &self.contents.raw_content;
impl RunValidationError {
/// Renders the GitHub input injection patterns found in a single `run:`
/// command as one diagnostic group.
pub fn annotation_group<'a>(&self, file_path: &Path, raw_content: &'a str) -> Group<'a> {
let mut identical_lines = HashMap::new();
let ranges = self
.errors
.found_injection_patterns
.iter()
.flat_map(|error| error.found_injection_patterns.iter())
.map(|(line, pattern_range)| {
let initial_offset = identical_lines
.get(&(line.as_str(), pattern_range.start))
@ -89,8 +42,8 @@ impl WorkflowValidationError {
Level::ERROR
.primary_title("Found GitHub input injection in run command")
.element(
Snippet::source(&self.contents.raw_content)
.path(self.file_path.display().to_string())
Snippet::source(raw_content)
.path(file_path.display().to_string())
.annotations(ranges.map(|range| {
AnnotationKind::Primary
.span(range)
@ -100,10 +53,6 @@ impl WorkflowValidationError {
}
}
pub struct RunValidationError {
found_injection_patterns: Vec<(String, Range<usize>)>,
}
pub fn validate_run_command(command: &str) -> Result<(), RunValidationError> {
let patterns: Vec<_> = command
.lines()

View file

@ -136,8 +136,8 @@ impl WorkflowFile {
.as_ref()
.expect("Workflow must have a name at this point");
let filename = format!(
"{}.yml",
workflow_name.rsplit("::").next().unwrap_or(workflow_name)
"{workflow_name}.yml",
workflow_name = workflow_name.rsplit("::").next().unwrap_or(workflow_name)
);
let workflow_path = workflow_folder.join(filename);

View file

@ -4,7 +4,9 @@ use crate::tasks::workflows::{
deploy_docs::deploy_docs_workflow_call,
release::{self, notify_on_failure},
runners,
steps::{CommonJobConditions, NamedJob, checkout_repo, dependant_job, named},
steps::{
CommonJobConditions, CommonPermissionSets, NamedJob, checkout_repo, dependant_job, named,
},
vars::{self, StepOutput, WorkflowInput},
};
@ -39,6 +41,7 @@ pub fn after_release() -> Workflow {
};
named::workflow()
.with_minimal_permissions()
.add_env(("TAG_NAME", TAG_NAME_ENV))
.add_env(("IS_PRERELEASE", IS_PRERELEASE_ENV))
.on(Event::default()

View file

@ -3,8 +3,8 @@ use gh_workflow::*;
use crate::tasks::workflows::{
runners,
steps::{
self, DownloadArtifactStep, FluentBuilder, IfNoFilesFound, NamedJob, RepositoryTarget,
TokenPermissions, UploadArtifactStep, ZippyGitIdentity, named, use_clang,
self, CommonPermissionSets, DownloadArtifactStep, FluentBuilder, IfNoFilesFound, NamedJob,
RepositoryTarget, TokenPermissions, UploadArtifactStep, ZippyGitIdentity, named, use_clang,
},
vars::{self, StepOutput, WorkflowInput},
};
@ -15,6 +15,7 @@ pub fn autofix_pr() -> Workflow {
let run_autofix = run_autofix(&pr_number, &run_clippy);
let commit_changes = commit_changes(&pr_number, &run_autofix);
named::workflow()
.with_minimal_permissions()
.run_name(format!("autofix PR #{pr_number}"))
.on(Event::default().workflow_dispatch(
WorkflowDispatch::default()
@ -91,6 +92,11 @@ fn run_autofix(pr_number: &WorkflowInput, run_clippy: &WorkflowInput) -> NamedJo
named::job(use_clang(
Job::default()
.runs_on(runners::LINUX_DEFAULT)
.permissions(
Permissions::default()
.contents(Level::Read)
.pull_requests(Level::Read),
)
.outputs([(
"has_changes".to_owned(),
"${{ steps.create-patch.outputs.has_changes }}".to_owned(),

View file

@ -2,7 +2,7 @@ use gh_workflow::*;
use crate::tasks::workflows::{
runners,
steps::{self, CheckoutStep, CommonJobConditions, named},
steps::{self, CheckoutStep, CommonJobConditions, CommonPermissionSets, named},
vars::{StepOutput, WorkflowInput},
};
@ -10,6 +10,7 @@ pub fn bump_patch_version() -> Workflow {
let branch = WorkflowInput::string("branch", None).description("Branch name to run on");
let bump_patch_version_job = run_bump_patch_version(&branch);
named::workflow()
.with_minimal_permissions()
.on(Event::default()
.workflow_dispatch(WorkflowDispatch::default().add_input(branch.name, branch.input())))
.concurrency(
@ -80,6 +81,7 @@ fn run_bump_patch_version(branch: &WorkflowInput) -> steps::NamedJob {
named::job(
Job::default()
.with_repository_owner_guard()
.permissions(Permissions::default().contents(Level::Write))
.runs_on(runners::LINUX_DEFAULT)
.add_step(authenticate)
.add_step(checkout_branch(branch, &token))

View file

@ -2,7 +2,7 @@ use gh_workflow::*;
use crate::tasks::workflows::{
runners,
steps::{self, named},
steps::{self, CommonPermissionSets, named},
vars::{self, StepOutput, WorkflowInput},
};
@ -17,6 +17,7 @@ pub fn bump_zed_version() -> Workflow {
let stable_job = promote_to_stable(&target, &versions_job, &outputs);
named::workflow()
.with_minimal_permissions()
.on(Event::default()
.workflow_dispatch(WorkflowDispatch::default().add_input(target.name, target.input())))
.add_job(versions_job.name, versions_job.job)
@ -142,6 +143,11 @@ fn bump_main(
target.expr(),
target.expr(),
)))
.permissions(
Permissions::default()
.contents(Level::Write)
.pull_requests(Level::Write),
)
.needs(vec![versions_job.name.clone()])
.runs_on(runners::LINUX_DEFAULT)
.add_step(authenticate)
@ -193,6 +199,7 @@ fn create_preview_branch(
target.expr(),
target.expr(),
)))
.permissions(Permissions::default().contents(Level::Write))
.needs(vec![versions_job.name.clone()])
.runs_on(runners::LINUX_DEFAULT)
.add_step(authenticate)
@ -250,6 +257,7 @@ fn promote_to_stable(
target.expr(),
target.expr(),
)))
.permissions(Permissions::default().contents(Level::Write))
.needs(vec![versions_job.name.clone()])
.runs_on(runners::LINUX_DEFAULT)
.add_step(authenticate)

View file

@ -2,7 +2,10 @@ use gh_workflow::*;
use crate::tasks::workflows::{
runners,
steps::{self, NamedJob, RepositoryTarget, TokenPermissions, ZippyGitIdentity, named},
steps::{
self, CommonPermissionSets, NamedJob, RepositoryTarget, TokenPermissions, ZippyGitIdentity,
named,
},
vars::{StepOutput, WorkflowInput},
};
@ -13,6 +16,7 @@ pub fn cherry_pick() -> Workflow {
let pr_number = WorkflowInput::string("pr_number", None);
let cherry_pick = run_cherry_pick(&branch, &commit, &channel);
named::workflow()
.with_minimal_permissions()
.run_name(format!("cherry_pick to {channel} #{pr_number}"))
.on(Event::default().workflow_dispatch(
WorkflowDispatch::default()

View file

@ -3,7 +3,7 @@ use gh_workflow::{Event, Job, Schedule, Workflow, WorkflowDispatch};
use crate::tasks::workflows::{
release::{ComplianceContext, add_compliance_steps},
runners,
steps::{self, CommonJobConditions, named},
steps::{self, CommonJobConditions, CommonPermissionSets, named},
vars::StepOutput,
};
@ -11,6 +11,7 @@ pub fn compliance_check() -> Workflow {
let check = scheduled_compliance_check();
named::workflow()
.with_minimal_permissions()
.on(Event::default()
.schedule([Schedule::new("30 17 * * 2")])
.workflow_dispatch(WorkflowDispatch::default()))

View file

@ -1,6 +1,6 @@
use gh_workflow::*;
use crate::tasks::workflows::steps::{CommonJobConditions, NamedJob, named};
use crate::tasks::workflows::steps::{CommonJobConditions, CommonPermissionSets, NamedJob, named};
use super::{runners, steps};
@ -9,6 +9,7 @@ pub fn danger() -> Workflow {
let danger = danger_job();
named::workflow()
.with_minimal_permissions()
.on(Event::default()
.pull_request(PullRequest::default().add_branch("main").types([
PullRequestType::Opened,

View file

@ -3,7 +3,8 @@ use indoc::indoc;
use crate::tasks::workflows::runners::{self, Platform};
use crate::tasks::workflows::steps::{
self, CommonJobConditions, FluentBuilder as _, NamedJob, dependant_job, named, use_clang,
self, CommonJobConditions, CommonPermissionSets, FluentBuilder as _, NamedJob, dependant_job,
named, use_clang,
};
use crate::tasks::workflows::vars;
@ -14,6 +15,7 @@ pub(crate) fn deploy_collab() -> Workflow {
let deploy = deploy(&[&publish]);
named::workflow()
.with_minimal_permissions()
.on(Event::default().push(Push::default().add_tag("collab-production")))
.add_env(("DOCKER_BUILDKIT", "1"))
.add_job(style.name, style.job)

View file

@ -6,8 +6,8 @@ use gh_workflow::{
use crate::tasks::workflows::{
runners,
steps::{
self, CommonJobConditions, FluentBuilder as _, NamedJob, UploadArtifactStep, named,
release_job,
self, CommonJobConditions, CommonPermissionSets, FluentBuilder as _, NamedJob,
UploadArtifactStep, named, release_job,
},
vars::{self, StepOutput, WorkflowInput},
};
@ -314,6 +314,7 @@ pub(crate) fn deploy_docs() -> Workflow {
let deploy_docs = deploy_docs_job(&channel, &checkout_ref);
named::workflow()
.with_minimal_permissions()
.add_event(
Event::default().workflow_dispatch(
WorkflowDispatch::default()
@ -366,6 +367,7 @@ pub(crate) fn deploy_nightly_docs() -> Workflow {
named::workflow()
.name("deploy_nightly_docs")
.permissions(Permissions::default())
.add_event(Event::default().push(Push::default().add_branch("main")))
.add_job(deploy_docs.name, deploy_docs.job)
}

View file

@ -8,7 +8,7 @@ use crate::tasks::workflows::{
extensions::WithAppSecrets,
run_tests::DETECT_CHANGED_EXTENSIONS_SCRIPT,
runners,
steps::{self, CommonJobConditions, NamedJob, named},
steps::{self, CommonJobConditions, CommonPermissionSets, NamedJob, named},
vars::{StepOutput, one_workflow_per_non_main_branch},
};
@ -20,6 +20,7 @@ pub(crate) fn extension_auto_bump() -> Workflow {
let bump = bump_extension_versions(&detect);
named::workflow()
.with_minimal_permissions()
.add_event(
Event::default().push(
Push::default()

View file

@ -5,9 +5,10 @@ use crate::tasks::workflows::{
extension_tests::{self},
runners,
steps::{
self, BASH_SHELL, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, GitHubScriptStep,
GitRef, NamedJob, RefSha, RepositoryTarget, cache_rust_dependencies_namespace,
checkout_repo, create_ref, dependant_job, generate_token, named,
self, BASH_SHELL, CommonJobConditions, CommonPermissionSets,
DEFAULT_REPOSITORY_OWNER_GUARD, GitHubScriptStep, GitRef, NamedJob, RefSha,
RepositoryTarget, cache_rust_dependencies_namespace, checkout_repo, create_ref,
dependant_job, generate_token, named,
},
vars::{
JobOutput, StepOutput, WorkflowInput, WorkflowSecret,
@ -58,6 +59,7 @@ pub(crate) fn extension_bump() -> Workflow {
);
named::workflow()
.with_minimal_permissions()
.add_event(
Event::default().workflow_call(
WorkflowCall::default()

View file

@ -8,7 +8,7 @@ use crate::tasks::workflows::{
},
runners,
steps::{
self, BASH_SHELL, CommonJobConditions, FluentBuilder, NamedJob,
self, BASH_SHELL, CommonJobConditions, CommonPermissionSets, FluentBuilder, NamedJob,
cache_rust_dependencies_namespace, named,
},
vars::{PathCondition, StepOutput, WorkflowInput, one_workflow_per_non_main_branch_and_token},
@ -41,6 +41,7 @@ pub(crate) fn extension_tests() -> Workflow {
let working_directory = WorkflowInput::string("working-directory", Some(".".to_owned()));
named::workflow()
.with_minimal_permissions()
.add_event(
Event::default().workflow_call(
WorkflowCall::default()

View file

@ -8,8 +8,8 @@ use serde_json::json;
use crate::tasks::workflows::steps::GitRef;
use crate::tasks::workflows::steps::RefSha;
use crate::tasks::workflows::steps::{
CheckoutStep, DownloadArtifactStep, IfNoFilesFound, ResultEncoding, TokenPermissions,
UploadArtifactStep, cache_rust_dependencies_namespace,
CheckoutStep, CommonPermissionSets, DownloadArtifactStep, IfNoFilesFound, ResultEncoding,
TokenPermissions, UploadArtifactStep, cache_rust_dependencies_namespace,
};
use crate::tasks::workflows::vars::JobOutput;
use crate::tasks::workflows::{
@ -42,6 +42,7 @@ pub(crate) fn extension_workflow_rollout() -> Workflow {
let create_tag = create_rollout_tag(&rollout_workflows, &filter_repos_input);
named::workflow()
.with_minimal_permissions()
.on(Event::default().workflow_dispatch(
WorkflowDispatch::default()
.add_input(filter_repos_input.name, filter_repos_input.input())

View file

@ -27,6 +27,7 @@ pub(crate) fn bump_version(args: &GenerateWorkflowArgs) -> Workflow {
)
.pull_request(PullRequest::default().add_type(PullRequestType::Labeled))
.workflow_dispatch(WorkflowDispatch::default()))
.permissions(Permissions::default())
.concurrency(one_workflow_per_non_main_branch_and_token("labels"))
.add_job(determine_bump_type.name, determine_bump_type.job)
.add_job(call_bump_version.name, call_bump_version.job)

View file

@ -9,6 +9,7 @@ use crate::tasks::workflows::{
pub(crate) fn run_tests(args: &GenerateWorkflowArgs) -> Workflow {
let call_extension_tests = call_extension_tests(args.sha.as_ref());
named::workflow()
.permissions(Permissions::default().contents(Level::Read))
.on(Event::default()
.pull_request(PullRequest::default().add_branch("**"))
.push(Push::default().add_branch("main")))

View file

@ -1,6 +1,6 @@
use crate::tasks::workflows::{
runners::{Arch, Platform},
steps::{CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob},
steps::{CommonJobConditions, CommonPermissionSets, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob},
};
use super::{runners, steps, steps::named, vars};
@ -13,6 +13,7 @@ use gh_workflow::*;
pub fn nix_build() -> Workflow {
let [nix_linux_x86_64, nix_mac_aarch64] = nix_pr_jobs(&["run-nix", "run-bundling"]);
named::workflow()
.with_minimal_permissions()
.on(Event::default().pull_request(
PullRequest::default().types([PullRequestType::Labeled, PullRequestType::Synchronize]),
))

View file

@ -4,8 +4,8 @@ use indoc::{formatdoc, indoc};
use crate::tasks::workflows::{
runners,
steps::{
self, DEFAULT_REPOSITORY_OWNER_GUARD, GitRef, NamedJob, RefSha, RepositoryTarget,
TokenPermissions, generate_token, named,
self, CommonPermissionSets, DEFAULT_REPOSITORY_OWNER_GUARD, GitRef, NamedJob, RefSha,
RepositoryTarget, TokenPermissions, generate_token, named,
},
vars::{self, StepOutput, WorkflowInput},
};
@ -22,6 +22,7 @@ pub fn publish_extension_cli() -> Workflow {
let update_sha_in_extensions = update_sha_in_extensions(&publish, &message);
named::workflow()
.with_minimal_permissions()
.on(Event::default().workflow_dispatch(
WorkflowDispatch::default().add_input(message.name, message.input()),
))

View file

@ -1,4 +1,6 @@
use gh_workflow::{Event, Expression, Level, Push, Run, Step, Use, Workflow, ctx::Context};
use gh_workflow::{
Event, Expression, Level, Permissions, Push, Run, Step, Use, Workflow, ctx::Context,
};
use indoc::formatdoc;
use crate::tasks::workflows::{
@ -6,8 +8,8 @@ use crate::tasks::workflows::{
run_tests,
runners::{self, Arch, Platform},
steps::{
self, DownloadArtifactStep, FluentBuilder, NamedJob, TokenPermissions, dependant_job,
named, release_job,
self, CommonPermissionSets, DownloadArtifactStep, FluentBuilder, NamedJob,
TokenPermissions, dependant_job, named, release_job,
},
vars::{self, JobOutput, StepOutput, assets},
};
@ -101,6 +103,7 @@ pub(crate) fn release() -> Workflow {
named::workflow()
.on(Event::default().push(Push::default().tags(vec!["v*".to_string()])))
.concurrency(vars::one_workflow_per_non_main_branch())
.with_minimal_permissions()
.add_env(("CARGO_TERM_COLOR", "always"))
.add_env(("RUST_BACKTRACE", "1"))
.add_job(macos_tests.name, macos_tests.job)
@ -466,6 +469,7 @@ fn upload_release_assets(deps: &[&NamedJob], bundle: &ReleaseBundleJobs) -> Name
named::job(
dependant_job(&deps)
.runs_on(runners::LINUX_MEDIUM)
.permissions(Permissions::default().contents(Level::Write))
.add_step(download_workflow_artifacts())
.add_step(steps::script("ls -lR ./artifacts"))
.add_step(prep_release_artifacts())

View file

@ -8,8 +8,8 @@ use crate::tasks::workflows::{
run_tests::run_platform_tests_no_filter,
runners::{Arch, Platform, ReleaseChannel},
steps::{
CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, GitRef, NamedJob,
RefSha, RepositoryTarget, TokenPermissions,
CommonJobConditions, CommonPermissionSets, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder,
GitRef, NamedJob, RefSha, RepositoryTarget, TokenPermissions,
},
};
@ -47,6 +47,7 @@ pub fn release_nightly() -> Workflow {
let notify_on_failure = notify_on_failure(&bundle.jobs());
named::workflow()
.with_minimal_permissions()
.on(Event::default()
// Fire 6 times a day
.schedule([Schedule::new("0 */4 * * *")])

View file

@ -3,7 +3,10 @@ use std::path::Path;
use crate::tasks::workflows::{
release::ReleaseBundleJobs,
runners::{Arch, Platform, ReleaseChannel},
steps::{FluentBuilder, IfNoFilesFound, NamedJob, UploadArtifactStep, dependant_job, named},
steps::{
CommonPermissionSets, FluentBuilder, IfNoFilesFound, NamedJob, UploadArtifactStep,
dependant_job, named,
},
vars::{self, assets, bundle_envs},
};
@ -23,6 +26,7 @@ pub fn run_bundling() -> Workflow {
windows_x86_64: bundle_windows(Arch::X86_64, None, &[]),
};
named::workflow()
.with_minimal_permissions()
.on(Event::default().pull_request(
PullRequest::default().types([PullRequestType::Labeled, PullRequestType::Synchronize]),
))

View file

@ -8,8 +8,8 @@ use serde_json::json;
use crate::tasks::workflows::{
steps::{
CommonJobConditions, cache_rust_dependencies_namespace, repository_owner_guard_expression,
use_clang,
CommonJobConditions, CommonPermissionSets, cache_rust_dependencies_namespace,
repository_owner_guard_expression, use_clang,
},
vars::{self, PathCondition},
};
@ -105,6 +105,7 @@ pub(crate) fn run_tests() -> Workflow {
); // could be more specific here?
named::workflow()
.with_minimal_permissions()
.add_event(
Event::default()
.push(

View file

@ -376,6 +376,16 @@ impl CommonJobConditions for Job {
}
}
pub trait CommonPermissionSets: Sized {
fn with_minimal_permissions(self) -> Self;
}
impl CommonPermissionSets for Workflow {
fn with_minimal_permissions(self) -> Self {
self.permissions(Permissions::default().contents(Level::Read))
}
}
pub(crate) fn release_job(deps: &[&NamedJob]) -> Job {
dependant_job(deps)
.with_repository_owner_guard()
@ -503,6 +513,7 @@ pub mod named {
.collect::<Vec<_>>()
.join("::"),
)
.permissions(Permissions::default())
.defaults(Defaults::default().run(RunDefaults::default().shell(BASH_SHELL)))
}