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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions LOCAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,39 @@ should still point at a local checkout of the codebase the Jira issue
concerns, since triage reads repository context (docs, existing issues,
PRs) regardless of which forge hosts the issue itself.

## Testing code agent with Jira

The code agent supports Jira Cloud as a work-item source via the
`event.source.system == "jira"` overlay in `harness/code.yaml`. Unlike
triage (which uses `FULLSEND_FORGE=jira`), the code agent keeps
`FULLSEND_FORGE` set to the target forge (`github` or `gitlab`) and uses
a separate `FULLSEND_TRACKER=jira` signal. The Jira overlay composes
with the target-forge overlay via merge-all-matching.

When the source tracker differs from the target forge, the code agent does not
require `ISSUE_NUMBER`. It derives the work-item key from
`FULLSEND_WORK_ITEM_URL`, uses it in the branch and PR, and does not treat the
external key as a GitHub or GitLab issue number.

```bash
# Jira-source env vars (runner-only — never enter sandbox)
export FULLSEND_WORK_ITEM_URL="https://your-site.atlassian.net/browse/TESTPROJ-42"
export JIRA_USER_EMAIL="you@example.com"
export JIRA_TOKEN="your-jira-api-token"
export JIRA_BASE_URL="https://your-site.atlassian.net"

# Target forge — the code agent still pushes/creates PRs on this forge
export FULLSEND_FORGE="github"
export GH_TOKEN="$(gh auth token)"
```

Run `fullsend run code` the same way as step 3 above. `--target-repo`
should point at a local checkout of the repo where the PR will be
created. The Jira pre-script fetches the issue via the `fullsend` CLI
and writes the context to `/tmp/jira-issue-context.json`, which
`host_files` copies into the sandbox as
`/sandbox/workspace/.issue-context.json`.

## Testing a new configuration option

When testing a new env var, verify both cases:
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.DEFAULT_GOAL := help
.PHONY: help script-build check-bundle script-test test

BUNDLE_SRCS := scripts/pre-code.src.sh scripts/post-code.src.sh scripts/pre-fix.src.sh scripts/post-fix.src.sh scripts/pre-prioritize.src.sh scripts/post-prioritize.src.sh scripts/pre-retro.src.sh scripts/post-retro.src.sh scripts/pre-review.src.sh scripts/post-review.src.sh scripts/pre-scribe.src.sh scripts/post-scribe.src.sh scripts/pre-triage.src.sh scripts/post-triage.src.sh scripts/validate-code-output.src.sh
BUNDLE_SRCS := scripts/pre-code.src.sh scripts/pre-code-jira.src.sh scripts/post-code.src.sh scripts/pre-fix.src.sh scripts/post-fix.src.sh scripts/pre-prioritize.src.sh scripts/post-prioritize.src.sh scripts/pre-retro.src.sh scripts/post-retro.src.sh scripts/pre-review.src.sh scripts/post-review.src.sh scripts/pre-scribe.src.sh scripts/post-scribe.src.sh scripts/pre-triage.src.sh scripts/post-triage.src.sh scripts/validate-code-output.src.sh
BUNDLE_OUTS := $(BUNDLE_SRCS:.src.sh=.sh)
LIB_DEPS := $(wildcard scripts/lib/*.lib.sh)

Expand Down Expand Up @@ -51,6 +51,7 @@ script-test:
$(call run-timed,bash scripts/pre-prioritize-test.sh)
$(call run-timed,bash scripts/post-prioritize-test.sh)
$(call run-timed,bash scripts/pre-code-test.sh)
$(call run-timed,bash scripts/pre-code-jira-test.sh)
$(call run-timed,bash scripts/post-code-test.sh)
$(call run-timed,bash scripts/pre-review-test.sh)
$(call run-timed,bash scripts/post-review-test.sh)
Expand Down
29 changes: 21 additions & 8 deletions docs/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ See [Customizing with AGENTS.md](https://fullsend.sh/docs/guides/user/customizin

| Variable | Description | Default | Valid values |
|----------|-------------|---------|--------------|
| `CODE_ALLOWED_TARGET_BRANCHES` | Restricts which branches the code agent can target when pushing. The post-code script validates the agent's chosen target branch against this variable before pushing. Set via `env.runner` in `harness/code.yaml` (never injected into the sandbox). | Repo default branch (auto-detected via forge API; falls back to `main`) | Comma-separated branch names (e.g. `main,develop`) or `*` for any branch |
| `FULLSEND_FORGE` | Forge platform. Set automatically by the harness `forge.<platform>.env` section. | (set by harness) | `"github"`, `"gitlab"` |
| `CODE_ALLOWED_TARGET_BRANCHES` | Restricts which branches the code agent can target when pushing. The post-code script reads it from the runner when present and validates the agent's chosen target branch before pushing. It is never injected into the sandbox. | Repo default branch (auto-detected via forge API; falls back to `main`) | Comma-separated branch names (e.g. `main,develop`) or `*` for any branch |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] missing-documentation

CODE_ALLOWED_TARGET_BRANCHES description updated to remove env.runner reference but does not explain how users should now set it.

Suggested fix: Add a note about the recommended override path.

| `FULLSEND_FORGE` | Forge platform. Set automatically by the harness overlay `env` section (matched via `when: 'runtime.forge == "<platform>"'`). | (set by harness) | `"github"`, `"gitlab"` |
| `FULLSEND_TRACKER` | Source tracker for the work item (matches triage convention). When set to `"jira"`, the code agent requires issue context at `/sandbox/workspace/.issue-context.json` (prepared by the Jira pre-script) instead of calling forge APIs. Set by the Jira-source overlay in `harness/code.yaml`. | (unset — forge-native) | `"jira"` |
| `ISSUE_NUMBER` | Numeric source issue identifier used when the source tracker is the target forge. It is optional for external-tracker runs because that work-item key is not a target-forge issue number. | (set by forge-native workflows) | Positive integer |
| `CODE_AUTO_MERGE` | Set to `"true"` to enable auto-merge on PRs/MRs created by the code agent. On GitHub, uses `gh pr merge --auto`; on GitLab, uses `merge_when_pipeline_succeeds`. Requires branch protection with required reviews or status checks on the target branch. Read directly from the runner environment (not declared in `env.runner`). | `""` (disabled) | `"true"` to enable |
| `CODE_AUTO_MERGE_METHOD` | Merge method for auto-merge: `"squash"`, `"rebase"`, or `"merge"`. When unset, auto-detected from the repo's allowed merge methods (prefers squash). Omitted automatically when the target branch uses a merge queue. Ignored unless `CODE_AUTO_MERGE` is `"true"`. | Auto-detected (prefers squash) | `"squash"`, `"rebase"`, `"merge"` |

Expand Down Expand Up @@ -170,16 +172,27 @@ The precedence is as follows:

## Multi-forge support

The code agent supports both GitHub and GitLab. The harness
`forge.<platform>` sections configure platform-specific policies,
skills, and env vars. Key differences from single-forge
setup:
The code agent supports both GitHub and GitLab, and can also consume
work items from Jira. The harness uses `overlays:` with CEL `when:`
expressions to configure platform-specific policies, skills, and env
vars. Key differences from single-forge setup:

- **`FULLSEND_FORGE`** is required. Set automatically by the harness
`forge.<platform>.env` section (`"github"` or `"gitlab"`).
- **`FULLSEND_FORGE`** is required. Set automatically by the matching
forge overlay's `env` section (`"github"` or `"gitlab"`).
- **`ISSUE_URL`** replaces `GITHUB_ISSUE_URL` in scripts. The
per-forge env file (`env/github/code.env` or `env/gitlab/code.env`)
maps the platform-specific variable to `ISSUE_URL`.
- **Jira-source overlay** — when the work item originates from Jira
(`event.source.system == "jira"`), a dedicated overlay fetches the
issue via `fullsend issues get --tracker jira` on the runner and
copies the context into the sandbox. Jira credentials stay on the
runner. The Jira overlay composes with the target-forge overlay
(GitHub or GitLab) via merge-all-matching.
- **External work-item identity** — when the source tracker differs from the
target forge, the code agent derives the key from `ISSUE_URL`.
Branch names and PR text use that key and link the source URL; they do not
invent a numeric target-forge issue reference. Target-forge issue comments
and assignee lookup are skipped when no such issue exists.
- **Policy** is per-forge: `policies/base.yaml` (GitHub) or
`policies/gitlab/code.yaml` (GitLab). Custom harnesses using `base:`
composition should override at the forge level if needed.
Expand Down
33 changes: 30 additions & 3 deletions harness/code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ env:

timeout_minutes: 35

forge:
github:
overlays:
- when: 'runtime.forge == "github"'
providers:
- providers/github-code.yaml
openshell:
Expand All @@ -107,7 +107,7 @@ forge:
ISSUE_URL: "${GITHUB_ISSUE_URL}"
GH_TOKEN: "${GH_TOKEN}"
FULLSEND_FORGE: github
gitlab:
- when: 'runtime.forge == "gitlab"'
policy: policies/gitlab/code.yaml
skills:
- skills/gitlab-forge
Expand All @@ -125,3 +125,30 @@ forge:
ISSUE_URL: "${GITLAB_ISSUE_URL}"
GITLAB_TOKEN: "${GITLAB_TOKEN}"
FULLSEND_FORGE: gitlab
# Jira-source overlay — fetches the work item from Jira and prepares
# issue context for the sandbox. Ordered AFTER forge overlays so its
# env vars (especially ISSUE_URL) win under last-writer-wins merge
# semantics when both the Jira overlay and a forge overlay match.
# pre_script overrides the top-level default. Post-script selection
# stays with the target forge (GitHub/GitLab).
- when: 'event.source.system == "jira"'
pre_script: scripts/pre-code-jira.sh
host_files:
- src: ${RUNNER_TEMP}/jira-issue-context.json
dest: /sandbox/workspace/.issue-context.json
env:
runner:
JIRA_ISSUE_CONTEXT_FILE: "${RUNNER_TEMP}/jira-issue-context.json"
JIRA_USER_EMAIL: "${JIRA_USER_EMAIL}"
JIRA_TOKEN: "${JIRA_TOKEN}"
JIRA_BASE_URL: "${JIRA_BASE_URL}"
ISSUE_URL: "${FULLSEND_WORK_ITEM_URL}"
# A Jira source work item has a key, not a target-forge issue number.
# Override the inherited compatibility variable so harness expansion
# does not require ISSUE_NUMBER for cross-forge runs.
ISSUE_NUMBER: ""
FULLSEND_TRACKER: jira
sandbox:
FULLSEND_TRACKER: jira
ISSUE_NUMBER: ""
ISSUE_URL: "${FULLSEND_WORK_ITEM_URL}"
8 changes: 8 additions & 0 deletions scripts/lib/post-failure-report.lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@ report_post_failure_to_issue() {
fi
POST_FAILURE_REPORTED=true

# An external tracker may have no corresponding target-forge issue. The
# workflow status notification remains the source-of-truth; do not guess a
# target issue number and risk commenting on unrelated work.
if [ "${EXTERNAL_WORK_ITEM:-false}" = "true" ]; then
gha_echo warning "Post-code failure for ${WORK_ITEM_KEY:-external work item}; see workflow logs"
return 0
fi

_post_failure_ensure_token

local category="${POST_FAILURE_CATEGORY:-post-script-error}"
Expand Down
44 changes: 37 additions & 7 deletions scripts/post-code-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,16 @@ fi
rewrite_title() {
local commit_subject="$1"
local issue_number="$2"
local identity_mode="${3:-forge-native}"

if echo "${commit_subject}" | grep -qE '^[a-z]+\('; then
echo "${commit_subject}"
elif echo "${commit_subject}" | grep -qE '^[a-z]+: '; then
echo "${commit_subject}" | sed "s/^\([a-z]*\): /\1(#${issue_number}): /"
if [ "${identity_mode}" = "external" ]; then
echo "${commit_subject}" | sed "s/^\([a-z]*\): /\1(${issue_number}): /"
else
echo "${commit_subject}" | sed "s/^\([a-z]*\): /\1(#${issue_number}): /"
fi
else
echo "${commit_subject}"
fi
Expand Down Expand Up @@ -166,6 +171,14 @@ run_test "ci-type" \
"10" \
"ci(#10): update workflow permissions"

actual_external_title="$(rewrite_title "fix: handle cross-forge work" "FSENDAI-4804" external)"
if [ "${actual_external_title}" != "fix(FSENDAI-4804): handle cross-forge work" ]; then
echo "FAIL: external-tracker-title-uses-work-item-key"
FAILURES=$((FAILURES + 1))
else
echo "PASS: external-tracker-title-uses-work-item-key"
fi

# ---------------------------------------------------------------------------
# Test helper — reimplements the PR body assembly logic from post-code.sh
# so we can test it without a git repo or network access.
Expand All @@ -178,6 +191,8 @@ build_pr_body() {
local pr_body_from_result="${5:-}" # optional: agent-provided pr_body
local pr_body_scan_status="${6:-skipped}" # passed|blocked|error|skipped
local closes_issue="${7:-true}" # optional: "true" or "false"
local identity_mode="${8:-forge-native}"
local issue_url="${9:-}"

local description=""
if [ -n "${pr_body_from_result}" ]; then
Expand All @@ -204,17 +219,23 @@ build_pr_body() {
# Fall back if pr_body was absent or stripped to empty
if [ -z "${description}" ]; then
if [ -z "${commit_body}" ]; then
description="Automated implementation for issue #${issue_number}."
if [ "${identity_mode}" = "external" ]; then
description="Automated implementation for ${issue_number}."
else
description="Automated implementation for issue #${issue_number}."
fi
else
description="${commit_body}"
fi
fi

local issue_ref_keyword
if [ "${closes_issue}" = "false" ]; then
issue_ref_keyword="Related to"
local issue_reference
if [ "${identity_mode}" = "external" ]; then
issue_reference="Related to ${issue_url}"
elif [ "${closes_issue}" = "false" ]; then
issue_reference="Related to #${issue_number}"
else
issue_ref_keyword="Closes"
issue_reference="Closes #${issue_number}"
fi

local pr_body_scan_line
Expand All @@ -229,7 +250,7 @@ build_pr_body() {

---

${issue_ref_keyword} #${issue_number}
${issue_reference}

### Post-script verification

Expand Down Expand Up @@ -298,6 +319,15 @@ run_body_test "empty-body-fallback" \
"99" "agent/99-add-feature" \
"Automated implementation for issue #99." "yes"

external_body="$(build_pr_body "" "FSENDAI-4804" "agent/FSENDAI-4804-fix" "abc123..def456" "" skipped true external "https://redhat.atlassian.net/browse/FSENDAI-4804")"
if ! grep -qF "Related to https://redhat.atlassian.net/browse/FSENDAI-4804" <<<"${external_body}" \
|| grep -qF "Closes #" <<<"${external_body}"; then
echo "FAIL: external-tracker-body-links-work-item-without-forge-close"
FAILURES=$((FAILURES + 1))
else
echo "PASS: external-tracker-body-links-work-item-without-forge-close"
fi

# Empty commit body should still not have Changed files
run_body_test "empty-body-no-changed-files" \
"" \
Expand Down
Loading
Loading