feat: Add support for local config #24
No reviewers
Labels
No labels
automerge
bug
dependencies
duplicate
enhancement
help wanted
invalid
migrations
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Personal/woodpecker-extension!24
Loading…
Reference in a new issue
No description provided.
Delete branch "repo-config"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Review Summary
This PR adds repository-label-based rule matching: repos can place a root-level
woodpecker.tomldefininglabels, which the extension fetches from the Forgejo Contents API (at the pipeline ref) and AND-combines with the existing glob-pattern rules via newmatch_labels/match_not_labelsfields. Overall the feature is well designed and documented: the rule engine changes are clean, the concurrency viatokio::try_join!is good, label conditions have unit tests, and the404 → no labelspath degrades gracefully.Key issues
Misleading inverted variable names in
forgejo_integration.rs(most important).create_configurationreturns(Configuration, String)built asOk((config, owner)), but both call sites destructure it aslet (owner, config) = ..., so the localowneractually holds theConfigurationandconfigholds the owner string. The code still works because the two swaps cancel out when passing(&owner, &config)to the generatedrepo_get_contents*functions (which expect(configuration, owner, ...)). This is a correctness trap for future maintainers and should be aligned.Unconditional extra Forgejo API call.
fetch_repo_labelsruns for every pipeline request even when no rule on the endpoint uses labels, adding a (usually 404) request per invocation. It should be gated on whether any referenced rule has label constraints.Minor notes
woodpecker.tomlmaps to 400 Bad Request (documented, but misattributes the fault to the request; 502 or treat-as-empty might be better).\n, not\r; CRLF content would fail.toml 0.8) whentoml 1.1.3is already in the tree via config-rs.flake.nixtooltip = "defaultToolchain"is unrelated to this feature (drive-by).match_labelsto thebun-astroexample rule silently changes example semantics for anyone copying it.404 → Nonepath inget_repo_config, and no end-to-end test exercising label matching through the full handler; the newevaluate_rulesintegration tests only use empty labels.Tests
Existing tests remain valid because wiremock's unmatched
woodpecker.tomlrequests fall through to a 404 which is treated as "no labels".*🤖 Review by opencode (opencode/deepseek-v4-flash-free) — verdict: changes
Code review findings from opencode.
@ -44,2 +44,4 @@url = {version = "2.5", features = ["serde"]}globset = {version = "0.4"}base64 = {version = "0.22"}toml = {version = "0.8"}[style]
toml = { version = "0.8" }adds a second TOML parser stack — the tree already containstoml 1.1.3via config-rs (see Cargo.lock). The 0.8 line also predates TOML 1.1 spec support. Prefer reusing the existing version or document why the split is needed.@ -39,1 +39,4 @@match_patterns = ["bun.lock", "package.json"]# All labels listed here must be present in the repository's root-level# woodpecker.toml file.match_labels = ["destination:cluster"][style] Adding
match_labels = ["destination:cluster"]to the existingbun-astroexample rule silently changes its semantics — anyone copying the example now needs a repo-rootwoodpecker.tomlwith that label for the rule to match. Consider a separate commented-out label example or a dedicated rule instead.@ -62,6 +62,7 @@;};devShells.default = mkDevShell {tooltip = "defaultToolchain";[style]
tooltip = "defaultToolchain"appears unrelated to this PR's feature. Confirm it is intentional (e.g. required by themkDevShellhelper) or drop it to keep the change scoped.@ -84,0 +118,4 @@fn create_configuration(forge_url: &Option<Nullable<String>>,netrc: &Option<Netrc>,) -> Result<(Configuration, String), AppError> {[style]
create_configurationreturnsOk((config, owner))typed as(Configuration, String), but both call sites destructure it aslet (owner, config) = ...(lines 67 and 85). This makes the localowneraConfigurationandconfigthe owner string. It only works because the double swap aligns with the generatedrepo_get_contents*signatures (configuration first, owner second). This is a latent correctness hazard for anyone refactoring the calls. Return/destructure in a consistent order (e.g.(owner, config)where owner is theString), or restructure the helper to make the binding obvious.@ -84,0 +106,4 @@)));};let decoded = base64::engine::general_purpose::STANDARD.decode(content.replace('\n', ""))[style]
content.replace('\n', "")only strips LF; CRLF-encoded content would leave\rand fail base64 decoding. Strip all whitespace (e.g.content.chars().filter(|c| !c.is_whitespace()).collect::<String>()) for robustness.@ -91,2 +91,4 @@)));}AppError::InvalidRepoConfig(message) => {return ProblemDetail::new_bad_request(Some(&message));[style] A malformed
woodpecker.toml(content owned by the repository, not the requester) is mapped to 400 Bad Request, which tells the Woodpecker server its request was bad. This is documented in the README as deliberate, but a 502 or treating malformed labels as 'no labels' would be less surprising for a repo-controlled artifact.@ -27,1 +27,3 @@let file_names = fetch_root_file_names(&client.forgejo, request).await?;let (file_names, labels) = tokio::try_join!(fetch_root_file_names(&client.forgejo, request),fetch_repo_labels(&client.forgejo, request)[performance]
fetch_repo_labelsis awaited unconditionally for every pipeline request, even when none of the endpoint's rules referencematch_labels/match_not_labels. Since most repos have nowoodpecker.toml, this adds a 404 round-trip to every invocation. Consider only fetching labels when at least one referenced rule defines label constraints.Review Summary
This PR adds repository-label based rule matching (
match_labels/match_not_labels), reading an optional root-levelwoodpecker.tomlfrom the repo at the pipeline ref. Overall the change is solid and well-executed: it is fully documented in the README, config examples are updated, OpenAPI documents the new 502 response, and there is good unit + wiremock test coverage for rule matching, the Forgejo client, the endpoint behavior, and the 502 path.Strong points:
labels_requiredgate inget_workflows_for_endpoint), and the two Forgejo calls are parallelized withtokio::try_join!.woodpecker.toml(404) is treated as an empty label list; matching keeps its existing AND semantics.create_configuration/extract_netrcreduces duplication.Issues worth addressing:
\nbut not\r(or other whitespace), so CRLF-wrapped content from some Forgejo deployments would failSTANDARDdecoding and turn an otherwise-fine repo into a permanent 502.woodpecker.tomlhard-fails the entire extension request (502) for that repo on any endpoint that has any label rule, even when no rule would have matched. This is documented as intentional, but treating unparseable TOML as empty labels + a warning log would be more resilient.match_labelsfor access control.flake.nixtooltip = "defaultToolchain"change is unrelated to this feature, and the TODO comment oncreate_configurationis now stale/ambiguous.*🤖 Review by opencode (opencode/deepseek-v4-flash-free) — verdict: comment
Code review findings from opencode.
@ -62,6 +62,7 @@;};[style] The
tooltip = "defaultToolchain"addition to the devShell appears unrelated to the repository-labels feature in this PR. Consider splitting it into a separate commit/PR to keep changes focused and reviewable.@ -163,1 +163,4 @@pub match_not_patterns: Vec<String>,/// Labels that must all be present in the repository's `woodpecker.toml`.#[serde(default)]pub match_labels: Vec<String>,[security]
match_labels/match_not_labelsare derived from repository-controlled content that any contributor with push access can modify (they can simply add labels towoodpecker.toml). This means label conditions provide no access control — they are pure opt-in routing. Worth an explicit note in the README so operators do not use label-gated rules (e.g. a "deploy to production" workflow gated only onmatch_labels) as a security boundary.@ -84,0 +114,4 @@}}// TODO this should be refactored into a shared utility[style] The
// TODO this should be refactored into a shared utilitycomment is now stale/misleading: this PR already extracted the sharedcreate_configuration/extract_netrchelpers used by both methods. Either drop the TODO or reword it to describe the actual remaining duplication.@ -84,0 +106,4 @@)));};let decoded = base64::engine::general_purpose::STANDARD.decode(content.replace('\n', ""))[bug]
content.replace('\n', "")only strips LF, so CRLF-wrapped base64 (which some Forgejo/Gitea deployments emit, e.g. MIME-style 76-char wrapping with\r\n) still contains\rcharacters. TheSTANDARDengine rejects any whitespace, so this turns intoInvalidRepoConfig→ 502 for an otherwise-valid file. Consider stripping all ASCII whitespace instead, e.g.content.chars().filter(|c| !c.is_ascii_whitespace()).collect::<String>().@ -143,0 +174,4 @@return Ok(Vec::new());};let config: RepoConfig =toml::from_str(&content).map_err(|error| AppError::InvalidRepoConfig(error.to_string()))?;[bug] A malformed
woodpecker.tomlaborts the whole request with 502 even when no rule would have matched for this repo, and it fails every pipeline of that repo on any endpoint with a label rule (the parse happens before rule evaluation and cannot be avoided by the repo operator). This is documented as intended, but a more resilient approach would be to log a warning and treat unparseable TOML as empty labels — especially since the file is repository-controlled upstream data that a broken push can introduce at any time.