Add cross-pillar review Woodpecker step #63

Merged
forgejo_admin merged 2 commits from 62-add-cross-pillar-review-woodpecker-step into main 2026-03-14 21:03:18 +00:00
Contributor

Summary

Adds a cross-pillar-review pipeline step that automatically creates a Forgejo issue when a merge to main changes files matching cross-pillar trigger patterns. This implements Level 2 of the cross-pillar trigger convention — automated detection replaces manual vigilance.

Closes #62

Changes

  • .woodpecker.yaml — added cross-pillar-review step after apply:
    • Image: alpine:3.19 (lightweight — only git, curl, jq)
    • When: push to main, depends on apply
    • Failure: failure: ignore so it never blocks the pipeline
    • Logic: git diff HEAD~1 --name-only detects changed files, matches against three trigger patterns (.woodpecker*, terraform/modules/*/main.tf, salt/), and creates a Forgejo issue with matched patterns, changed files list (truncated at 50), and a review checklist
    • API: Uses internal cluster URL (forgejo-http.forgejo.svc.cluster.local) matching existing plan step pattern

Pre-requisite

The forgejo_token Woodpecker repo secret currently has events: [pull_request] only. It must be updated to events: [push, pull_request] before this step can access the token on push-to-main pipelines. This can be done via Woodpecker UI or mcp__woodpecker__update_repo_secret.

Test Plan

  • Verify forgejo_token secret has push events enabled
  • Merge a test PR touching .woodpecker.yaml and confirm a [Cross-Pillar Review] issue is auto-created
  • Verify merging a PR that only touches terraform/k3s.tfvars does NOT create an issue
  • Verify step failure does not block the pipeline (confirm via failure: ignore)
  • No tofu validate needed — this is a .woodpecker.yaml-only change with no Terraform modifications

Review Checklist

  • No unrelated changes
  • YAML syntax validated
  • Matches existing .woodpecker.yaml code style ($$ escaping, environment block, command list)
  • Uses internal cluster Forgejo URL (same pattern as plan step)
  • forgejo_token secret events updated to include push (pre-requisite, not in this PR)
  • Plan: plan-pal-e-agency Phase 9 → 9h
  • Forgejo issue: #62
  • Convention: convention-cross-pillar-triggers
## Summary Adds a `cross-pillar-review` pipeline step that automatically creates a Forgejo issue when a merge to main changes files matching cross-pillar trigger patterns. This implements Level 2 of the cross-pillar trigger convention — automated detection replaces manual vigilance. Closes #62 ## Changes - `.woodpecker.yaml` — added `cross-pillar-review` step after `apply`: - **Image:** `alpine:3.19` (lightweight — only git, curl, jq) - **When:** push to main, depends on `apply` - **Failure:** `failure: ignore` so it never blocks the pipeline - **Logic:** `git diff HEAD~1 --name-only` detects changed files, matches against three trigger patterns (`.woodpecker*`, `terraform/modules/*/main.tf`, `salt/`), and creates a Forgejo issue with matched patterns, changed files list (truncated at 50), and a review checklist - **API:** Uses internal cluster URL (`forgejo-http.forgejo.svc.cluster.local`) matching existing plan step pattern ## Pre-requisite The `forgejo_token` Woodpecker repo secret currently has `events: [pull_request]` only. It must be updated to `events: [push, pull_request]` before this step can access the token on push-to-main pipelines. This can be done via Woodpecker UI or `mcp__woodpecker__update_repo_secret`. ## Test Plan - [ ] Verify `forgejo_token` secret has push events enabled - [ ] Merge a test PR touching `.woodpecker.yaml` and confirm a `[Cross-Pillar Review]` issue is auto-created - [ ] Verify merging a PR that only touches `terraform/k3s.tfvars` does NOT create an issue - [ ] Verify step failure does not block the pipeline (confirm via `failure: ignore`) - No `tofu validate` needed — this is a `.woodpecker.yaml`-only change with no Terraform modifications ## Review Checklist - [x] No unrelated changes - [x] YAML syntax validated - [x] Matches existing `.woodpecker.yaml` code style (`$$` escaping, environment block, command list) - [x] Uses internal cluster Forgejo URL (same pattern as plan step) - [ ] `forgejo_token` secret events updated to include `push` (pre-requisite, not in this PR) ## Related - Plan: `plan-pal-e-agency` Phase 9 → 9h - Forgejo issue: #62 - Convention: `convention-cross-pillar-triggers`
Add cross-pillar review Woodpecker step
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
564f53432b
Add a new `cross-pillar-review` step that runs after `apply` on pushes
to main. It detects changes to cross-pillar trigger patterns
(.woodpecker*, terraform/modules/*/main.tf, salt/) and auto-creates a
Forgejo issue with matched patterns, changed files, and a review
checklist. Uses `failure: ignore` so it never blocks the pipeline.

Closes #62

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Contributor

Tofu Plan Output

tailscale_acl.this: Refreshing state... [id=acl]
helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin]
kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama]
kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak]
kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring]
kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo]
kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor]
kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale]
kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system]
kubernetes_namespace_v1.minio: Refreshing state... [id=minio]
kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres]
kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker]
data.kubernetes_namespace_v1.pal_e_docs: Reading...
data.kubernetes_namespace_v1.tofu_state: Reading...
kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin]
kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data]
kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak]
data.kubernetes_namespace_v1.pal_e_docs: Read complete after 0s [id=pal-e-docs]
helm_release.forgejo: Refreshing state... [id=forgejo]
data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state]
helm_release.loki_stack: Refreshing state... [id=loki-stack]
helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator]
helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack]
kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-docs/paledocs-db-url]
helm_release.cnpg: Refreshing state... [id=cnpg]
kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak]
helm_release.ollama: Refreshing state... [id=ollama]
helm_release.woodpecker: Refreshing state... [id=woodpecker]
kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel]
kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel]
kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource]
kubernetes_config_map_v1.pal_e_docs_dashboard: Refreshing state... [id=monitoring/pal-e-docs-dashboard]
helm_release.harbor: Refreshing state... [id=harbor]
kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard]
helm_release.minio: Refreshing state... [id=minio]
kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel]
kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter]
kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel]
kubernetes_manifest.dora_exporter_service_monitor: Refreshing state...
kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel]
minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups]
minio_s3_bucket.assets: Refreshing state... [id=assets]
minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup]
minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal]
minio_iam_user.cnpg: Refreshing state... [id=cnpg]
minio_iam_user.tf_backup: Refreshing state... [id=tf-backup]
minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal]
kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel]
kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel]
minio_iam_user_policy_attachment.cnpg: Refreshing state... [id=cnpg-20260302210642491000000001]
minio_iam_user_policy_attachment.tf_backup: Refreshing state... [id=tf-backup-20260314163610110100000001]
kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-s3-creds]
kubernetes_secret_v1.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds]
kubernetes_cron_job_v1.tf_state_backup: Refreshing state... [id=tofu-state/tf-state-backup]
kubernetes_cron_job_v1.cnpg_backup_verify: Refreshing state... [id=postgres/cnpg-backup-verify]
kubernetes_ingress_v1.harbor_funnel: Refreshing state... [id=harbor/harbor-funnel]

No changes. Your infrastructure matches the configuration.

OpenTofu has compared your real infrastructure against your configuration and
found no differences, so no changes are needed.
## Tofu Plan Output ``` tailscale_acl.this: Refreshing state... [id=acl] helm_release.nvidia_device_plugin: Refreshing state... [id=nvidia-device-plugin] kubernetes_namespace_v1.ollama: Refreshing state... [id=ollama] kubernetes_namespace_v1.keycloak: Refreshing state... [id=keycloak] kubernetes_namespace_v1.monitoring: Refreshing state... [id=monitoring] kubernetes_namespace_v1.forgejo: Refreshing state... [id=forgejo] kubernetes_namespace_v1.harbor: Refreshing state... [id=harbor] kubernetes_namespace_v1.tailscale: Refreshing state... [id=tailscale] kubernetes_namespace_v1.cnpg_system: Refreshing state... [id=cnpg-system] kubernetes_namespace_v1.minio: Refreshing state... [id=minio] kubernetes_namespace_v1.postgres: Refreshing state... [id=postgres] kubernetes_namespace_v1.woodpecker: Refreshing state... [id=woodpecker] data.kubernetes_namespace_v1.pal_e_docs: Reading... data.kubernetes_namespace_v1.tofu_state: Reading... kubernetes_secret_v1.keycloak_admin: Refreshing state... [id=keycloak/keycloak-admin] kubernetes_persistent_volume_claim_v1.keycloak_data: Refreshing state... [id=keycloak/keycloak-data] kubernetes_service_v1.keycloak: Refreshing state... [id=keycloak/keycloak] data.kubernetes_namespace_v1.pal_e_docs: Read complete after 0s [id=pal-e-docs] helm_release.forgejo: Refreshing state... [id=forgejo] data.kubernetes_namespace_v1.tofu_state: Read complete after 0s [id=tofu-state] helm_release.loki_stack: Refreshing state... [id=loki-stack] helm_release.tailscale_operator: Refreshing state... [id=tailscale-operator] helm_release.kube_prometheus_stack: Refreshing state... [id=kube-prometheus-stack] kubernetes_service_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] kubernetes_secret_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] kubernetes_secret_v1.paledocs_db_url: Refreshing state... [id=pal-e-docs/paledocs-db-url] helm_release.cnpg: Refreshing state... [id=cnpg] kubernetes_role_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_service_account_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_role_binding_v1.tf_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_deployment_v1.keycloak: Refreshing state... [id=keycloak/keycloak] helm_release.ollama: Refreshing state... [id=ollama] helm_release.woodpecker: Refreshing state... [id=woodpecker] kubernetes_ingress_v1.keycloak_funnel: Refreshing state... [id=keycloak/keycloak-funnel] kubernetes_ingress_v1.forgejo_funnel: Refreshing state... [id=forgejo/forgejo-funnel] kubernetes_config_map_v1.grafana_loki_datasource: Refreshing state... [id=monitoring/grafana-loki-datasource] kubernetes_config_map_v1.pal_e_docs_dashboard: Refreshing state... [id=monitoring/pal-e-docs-dashboard] helm_release.harbor: Refreshing state... [id=harbor] kubernetes_config_map_v1.dora_dashboard: Refreshing state... [id=monitoring/dora-dashboard] helm_release.minio: Refreshing state... [id=minio] kubernetes_ingress_v1.alertmanager_funnel: Refreshing state... [id=monitoring/alertmanager-funnel] kubernetes_deployment_v1.dora_exporter: Refreshing state... [id=monitoring/dora-exporter] kubernetes_ingress_v1.grafana_funnel: Refreshing state... [id=monitoring/grafana-funnel] kubernetes_manifest.dora_exporter_service_monitor: Refreshing state... kubernetes_ingress_v1.woodpecker_funnel: Refreshing state... [id=woodpecker/woodpecker-funnel] minio_s3_bucket.tf_state_backups: Refreshing state... [id=tf-state-backups] minio_s3_bucket.assets: Refreshing state... [id=assets] minio_iam_policy.tf_backup: Refreshing state... [id=tf-backup] minio_s3_bucket.postgres_wal: Refreshing state... [id=postgres-wal] minio_iam_user.cnpg: Refreshing state... [id=cnpg] minio_iam_user.tf_backup: Refreshing state... [id=tf-backup] minio_iam_policy.cnpg_wal: Refreshing state... [id=cnpg-wal] kubernetes_ingress_v1.minio_funnel: Refreshing state... [id=minio/minio-funnel] kubernetes_ingress_v1.minio_api_funnel: Refreshing state... [id=minio/minio-api-funnel] minio_iam_user_policy_attachment.cnpg: Refreshing state... [id=cnpg-20260302210642491000000001] minio_iam_user_policy_attachment.tf_backup: Refreshing state... [id=tf-backup-20260314163610110100000001] kubernetes_secret_v1.tf_backup_s3_creds: Refreshing state... [id=tofu-state/tf-backup-s3-creds] kubernetes_secret_v1.cnpg_s3_creds: Refreshing state... [id=postgres/cnpg-s3-creds] kubernetes_cron_job_v1.tf_state_backup: Refreshing state... [id=tofu-state/tf-state-backup] kubernetes_cron_job_v1.cnpg_backup_verify: Refreshing state... [id=postgres/cnpg-backup-verify] kubernetes_ingress_v1.harbor_funnel: Refreshing state... [id=harbor/harbor-funnel] No changes. Your infrastructure matches the configuration. OpenTofu has compared your real infrastructure against your configuration and found no differences, so no changes are needed. ```
Author
Contributor

Self-Review

Verdict: LGTM

Checked

  • YAML syntax validates cleanly (Python yaml.safe_load)
  • failure: ignore is a valid Woodpecker step-level directive (confirmed in docs)
  • CI_PIPELINE_URL is a valid Woodpecker built-in env var (confirmed in docs)
  • $$ escaping matches existing step style (plan step uses same pattern)
  • Internal cluster Forgejo URL matches plan step's API call pattern
  • depends_on: apply ensures step runs after deploy
  • || true on curl provides defense-in-depth alongside failure: ignore
  • Trigger patterns match all three from issue spec: .woodpecker*, terraform/modules/*/main.tf, salt/*
  • Changed files truncation at 50 lines handles large merges
  • Title truncation at 255 chars handles Forgejo API limit
  • Uses jq --rawfile for body (same pattern as plan step's --rawfile) -- avoids heredoc-in-YAML issues
  • No unrelated changes (single file, 110 additions, 0 deletions)

Pre-requisite (not in this PR)

The forgejo_token Woodpecker repo secret needs events: [push, pull_request] -- currently only [pull_request]. Must be updated before this step can access the token on push-to-main pipelines.

## Self-Review **Verdict: LGTM** ### Checked - YAML syntax validates cleanly (Python `yaml.safe_load`) - `failure: ignore` is a valid Woodpecker step-level directive (confirmed in docs) - `CI_PIPELINE_URL` is a valid Woodpecker built-in env var (confirmed in docs) - `$$` escaping matches existing step style (plan step uses same pattern) - Internal cluster Forgejo URL matches plan step's API call pattern - `depends_on: apply` ensures step runs after deploy - `|| true` on curl provides defense-in-depth alongside `failure: ignore` - Trigger patterns match all three from issue spec: `.woodpecker*`, `terraform/modules/*/main.tf`, `salt/*` - Changed files truncation at 50 lines handles large merges - Title truncation at 255 chars handles Forgejo API limit - Uses `jq --rawfile` for body (same pattern as plan step's `--rawfile`) -- avoids heredoc-in-YAML issues - No unrelated changes (single file, 110 additions, 0 deletions) ### Pre-requisite (not in this PR) The `forgejo_token` Woodpecker repo secret needs `events: [push, pull_request]` -- currently only `[pull_request]`. Must be updated before this step can access the token on push-to-main pipelines.
Author
Contributor

PR #63 Review

BLOCKERS

1. git diff HEAD~1 will fail on shallow clone (silent no-op)

Woodpecker's default clone step uses --depth=1. With only one commit in history, HEAD~1 does not exist, so git diff HEAD~1 --name-only will exit with a fatal error. Because failure: ignore is set, the step will silently fail on every single run without creating any issue. The feature will appear deployed but never actually fire.

Fix options (in order of simplicity):

  • Replace git diff HEAD~1 --name-only with git diff-tree --no-commit-id --name-only -r HEAD -- this command inspects the commit's own tree diff and works correctly in shallow clones.
  • Alternatively, override the clone depth with a clone: block at the top of .woodpecker.yaml.

2. printf "$$REASONS" interprets % as format specifiers

Line 205 (printf "$$REASONS") will mangle any filename containing % characters. For example, a file named salt/100%done.sls would cause printf to interpret %d as an integer format specifier, producing garbled output or an error.

Fix: Use printf '%s' "$$REASONS" instead of printf "$$REASONS".

NITS

1. forgejo_token secret event scope not enforced by this PR

The PR body correctly documents that forgejo_token must be updated to include push events, but this is a runtime prerequisite that lives outside the diff. If forgotten, the step will silently fail (empty token, 401 from Forgejo API, swallowed by || true). Consider adding a comment in the YAML near the from_secret line to make this dependency visible to future readers.

2. terraform/modules/*/main.tf pattern matches no current files

The glob terraform/modules/*/main.tf does not match any files in the current repo (there is no terraform/modules/ directory -- all .tf files are at terraform/ root). This is presumably forward-looking for when modules are extracted, which is fine, but worth noting that this trigger pattern is currently inert.

3. No deduplication guard for repeated merges

If two consecutive merges to main both touch salt/ files, two separate [Cross-Pillar Review] issues will be created. There is no check for an existing open issue with the same title prefix. This may be intentional (each merge deserves its own review), but could generate noise during high-activity periods. A comment in the YAML noting this design decision would help future readers.

4. curl -s hides HTTP errors

The curl call uses -s (silent) and || true, so a 4xx/5xx response from Forgejo will be silently swallowed. Consider adding -f (fail on HTTP errors) or logging the response body so that pipeline logs contain diagnostic information when the step does fire but fails to create the issue.

SOP COMPLIANCE

  • Branch named after issue (62-add-cross-pillar-review-woodpecker-step references #62)
  • PR body has Summary, Changes, Test Plan, Related sections
  • Related references plan slug (plan-pal-e-agency Phase 9 -> 9h)
  • Closes #62 present
  • No secrets or .env files committed
  • No scope creep (single file, .woodpecker.yaml only)
  • No Terraform changes, so tofu plan/tofu validate not required
  • Convention compliance (convention-cross-pillar-triggers Level 2) -- cannot verify issue format against convention spec (pal-e-docs not accessible to QA agent), but the issue template includes trigger patterns, changed files, and a review checklist which appears reasonable

VERDICT: NOT APPROVED

Two blockers must be fixed before merge:

  1. The shallow clone issue means the step will silently fail on every run -- it will never work as deployed. Replace git diff HEAD~1 with git diff-tree --no-commit-id --name-only -r HEAD.
  2. The printf format string injection is a correctness bug for filenames containing %.
## PR #63 Review ### BLOCKERS **1. `git diff HEAD~1` will fail on shallow clone (silent no-op)** Woodpecker's default clone step uses `--depth=1`. With only one commit in history, `HEAD~1` does not exist, so `git diff HEAD~1 --name-only` will exit with a fatal error. Because `failure: ignore` is set, the step will silently fail on every single run without creating any issue. The feature will appear deployed but never actually fire. Fix options (in order of simplicity): - Replace `git diff HEAD~1 --name-only` with `git diff-tree --no-commit-id --name-only -r HEAD` -- this command inspects the commit's own tree diff and works correctly in shallow clones. - Alternatively, override the clone depth with a `clone:` block at the top of `.woodpecker.yaml`. **2. `printf "$$REASONS"` interprets `%` as format specifiers** Line 205 (`printf "$$REASONS"`) will mangle any filename containing `%` characters. For example, a file named `salt/100%done.sls` would cause `printf` to interpret `%d` as an integer format specifier, producing garbled output or an error. Fix: Use `printf '%s' "$$REASONS"` instead of `printf "$$REASONS"`. ### NITS **1. `forgejo_token` secret event scope not enforced by this PR** The PR body correctly documents that `forgejo_token` must be updated to include `push` events, but this is a runtime prerequisite that lives outside the diff. If forgotten, the step will silently fail (empty token, 401 from Forgejo API, swallowed by `|| true`). Consider adding a comment in the YAML near the `from_secret` line to make this dependency visible to future readers. **2. `terraform/modules/*/main.tf` pattern matches no current files** The glob `terraform/modules/*/main.tf` does not match any files in the current repo (there is no `terraform/modules/` directory -- all `.tf` files are at `terraform/` root). This is presumably forward-looking for when modules are extracted, which is fine, but worth noting that this trigger pattern is currently inert. **3. No deduplication guard for repeated merges** If two consecutive merges to main both touch `salt/` files, two separate `[Cross-Pillar Review]` issues will be created. There is no check for an existing open issue with the same title prefix. This may be intentional (each merge deserves its own review), but could generate noise during high-activity periods. A comment in the YAML noting this design decision would help future readers. **4. `curl -s` hides HTTP errors** The `curl` call uses `-s` (silent) and `|| true`, so a 4xx/5xx response from Forgejo will be silently swallowed. Consider adding `-f` (fail on HTTP errors) or logging the response body so that pipeline logs contain diagnostic information when the step does fire but fails to create the issue. ### SOP COMPLIANCE - [x] Branch named after issue (`62-add-cross-pillar-review-woodpecker-step` references #62) - [x] PR body has Summary, Changes, Test Plan, Related sections - [x] Related references plan slug (`plan-pal-e-agency` Phase 9 -> 9h) - [x] `Closes #62` present - [x] No secrets or `.env` files committed - [x] No scope creep (single file, `.woodpecker.yaml` only) - [x] No Terraform changes, so `tofu plan`/`tofu validate` not required - [ ] Convention compliance (`convention-cross-pillar-triggers` Level 2) -- cannot verify issue format against convention spec (pal-e-docs not accessible to QA agent), but the issue template includes trigger patterns, changed files, and a review checklist which appears reasonable ### VERDICT: NOT APPROVED Two blockers must be fixed before merge: 1. The shallow clone issue means the step will silently fail on every run -- it will never work as deployed. Replace `git diff HEAD~1` with `git diff-tree --no-commit-id --name-only -r HEAD`. 2. The `printf` format string injection is a correctness bug for filenames containing `%`.
Fix shallow clone and printf format injection in cross-pillar-review step
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
4fc73bb3a4
- Replace `git diff HEAD~1` with `git diff-tree --no-commit-id --name-only -r HEAD`
  to work in Woodpecker's default --depth=1 shallow clones
- Add '%s' format specifier to `printf` to prevent interpretation of `%`
  characters in filenames as format specifiers

Addresses QA blockers on PR #63.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Contributor

QA Blockers Fixed

Both blockers from the QA review have been resolved in commit 4fc73bb:

  1. Shallow clone fix -- Replaced git diff HEAD~1 --name-only with git diff-tree --no-commit-id --name-only -r HEAD. The diff-tree command inspects the commit's own tree diff and works correctly in Woodpecker's default --depth=1 shallow clones.

  2. printf format string injection -- Changed printf "$$REASONS" to printf '%s' "$$REASONS". Filenames containing % characters (e.g., salt/100%done.sls) will no longer be misinterpreted as format specifiers.

## QA Blockers Fixed Both blockers from the QA review have been resolved in commit `4fc73bb`: 1. **Shallow clone fix** -- Replaced `git diff HEAD~1 --name-only` with `git diff-tree --no-commit-id --name-only -r HEAD`. The `diff-tree` command inspects the commit's own tree diff and works correctly in Woodpecker's default `--depth=1` shallow clones. 2. **printf format string injection** -- Changed `printf "$$REASONS"` to `printf '%s' "$$REASONS"`. Filenames containing `%` characters (e.g., `salt/100%done.sls`) will no longer be misinterpreted as format specifiers.
Author
Contributor

PR #63 Re-Review

Re-review after fixes for the two blockers identified in the previous review.

BLOCKER FIXES VERIFIED

1. Shallow clone fix (line 141)
git diff HEAD~1 --name-only has been replaced with git diff-tree --no-commit-id --name-only -r HEAD. This is correct -- diff-tree operates on the commit object directly and does not require parent commit history to be present, so it works in shallow clones (Woodpecker's default). The flags are appropriate: --no-commit-id suppresses the SHA line, --name-only outputs just file paths, -r recurses into subtrees.

2. Printf injection fix (line 205)
printf "$$REASONS" has been replaced with printf '%s' "$$REASONS". This is correct -- the %s format specifier ensures the variable contents are treated as a literal string, not as a format string. Any % characters in filenames or constructed text will not be interpreted as printf conversion specifiers.

Both fixes are surgical -- no other lines were changed, and no regressions were introduced.

BLOCKERS

None.

NITS

  1. echo "$$CHANGED_LIST" (line 209) -- This variable is constructed with literal \n sequences (lines 187, 189). In Alpine's busybox echo, \n is interpreted as a newline, which happens to be the desired behavior here for formatting. Worth a comment noting this relies on busybox echo behavior, but non-blocking since the image is pinned to alpine:3.19.

SOP COMPLIANCE

  • Branch named after issue (62-add-cross-pillar-review-woodpecker-step matches issue #62)
  • PR body has: Summary, Changes, Test Plan, Related
  • Related references plan slug (plan-pal-e-agency Phase 9 -> 9h)
  • No secrets committed (only from_secret references, no plaintext credentials)
  • Single file changed, no scope creep
  • Closes #62 present in PR body
  • No Terraform changes, so tofu plan/tofu validate not required

VERDICT: APPROVED

## PR #63 Re-Review Re-review after fixes for the two blockers identified in the previous review. ### BLOCKER FIXES VERIFIED **1. Shallow clone fix (line 141)** `git diff HEAD~1 --name-only` has been replaced with `git diff-tree --no-commit-id --name-only -r HEAD`. This is correct -- `diff-tree` operates on the commit object directly and does not require parent commit history to be present, so it works in shallow clones (Woodpecker's default). The flags are appropriate: `--no-commit-id` suppresses the SHA line, `--name-only` outputs just file paths, `-r` recurses into subtrees. **2. Printf injection fix (line 205)** `printf "$$REASONS"` has been replaced with `printf '%s' "$$REASONS"`. This is correct -- the `%s` format specifier ensures the variable contents are treated as a literal string, not as a format string. Any `%` characters in filenames or constructed text will not be interpreted as printf conversion specifiers. Both fixes are surgical -- no other lines were changed, and no regressions were introduced. ### BLOCKERS None. ### NITS 1. **`echo "$$CHANGED_LIST"` (line 209)** -- This variable is constructed with literal `\n` sequences (lines 187, 189). In Alpine's busybox `echo`, `\n` is interpreted as a newline, which happens to be the desired behavior here for formatting. Worth a comment noting this relies on busybox echo behavior, but non-blocking since the image is pinned to `alpine:3.19`. ### SOP COMPLIANCE - [x] Branch named after issue (`62-add-cross-pillar-review-woodpecker-step` matches issue #62) - [x] PR body has: Summary, Changes, Test Plan, Related - [x] Related references plan slug (`plan-pal-e-agency` Phase 9 -> 9h) - [x] No secrets committed (only `from_secret` references, no plaintext credentials) - [x] Single file changed, no scope creep - [x] `Closes #62` present in PR body - [x] No Terraform changes, so `tofu plan`/`tofu validate` not required ### VERDICT: APPROVED
forgejo_admin deleted branch 62-add-cross-pillar-review-woodpecker-step 2026-03-14 21:03:18 +00:00
Sign in to join this conversation.
No description provided.