Skip to content

Commit 5cd68bd

Browse files
ggallenclaude
andcommitted
fix: load per-repo config.yaml in fullsend run and reusable workflows
Per-repo installations store config.yaml in .fullsend/ but the CLI only parsed OrgConfig format. Add OrgConfigFromPerRepo adapter and teach tryLoadConfig/requireConfig to fall back to PerRepoConfig parsing. Update all six reusable workflows to layer workspace files under .fullsend/ when install_mode is per-repo, and pass fullsend-dir to the action invocation. Closes #2970 Signed-off-by: Greg Allen <greg@fullsend.ai> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Greg Allen <gallen@redhat.com>
1 parent 6cfeae9 commit 5cd68bd

11 files changed

Lines changed: 260 additions & 50 deletions

File tree

.github/workflows/reusable-code.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,14 @@ jobs:
9393
fi
9494
SRC=".defaults/internal/scaffold/fullsend-repo"
9595
LAYERED_DIRS="agents skills schemas harness plugins policies scripts env"
96+
DEST=""
97+
if [[ "${INSTALL_MODE}" == "per-repo" ]]; then
98+
DEST=".fullsend/"
99+
fi
96100
for dir in ${LAYERED_DIRS}; do
97101
if [[ -d "${SRC}/${dir}" ]]; then
98-
mkdir -p "${dir}"
99-
cp -r "${SRC}/${dir}/." "${dir}/"
102+
mkdir -p "${DEST}${dir}"
103+
cp -r "${SRC}/${dir}/." "${DEST}${dir}/"
100104
fi
101105
done
102106
CUSTOM_BASE="customized"
@@ -108,8 +112,8 @@ jobs:
108112
find "${CUSTOM_BASE}/${dir}" -type f ! -name '.gitkeep' -print0 \
109113
| while IFS= read -r -d '' f; do
110114
rel="${f#"${CUSTOM_BASE}"/}"
111-
mkdir -p "$(dirname "${rel}")"
112-
cp "${f}" "${rel}"
115+
mkdir -p "$(dirname "${DEST}${rel}")"
116+
cp "${f}" "${DEST}${rel}"
113117
done
114118
fi
115119
done
@@ -195,6 +199,7 @@ jobs:
195199
with:
196200
agent: code
197201
version: ${{ inputs.fullsend_version }}
202+
fullsend-dir: ${{ inputs.install_mode == 'per-repo' && '.fullsend' || '' }}
198203
run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
199204
status-repo: ${{ inputs.source_repo }}
200205
status-number: ${{ fromJSON(inputs.event_payload).issue.number }}

.github/workflows/reusable-fix.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,14 @@ jobs:
109109
fi
110110
SRC=".defaults/internal/scaffold/fullsend-repo"
111111
LAYERED_DIRS="agents skills schemas harness plugins policies scripts env"
112+
DEST=""
113+
if [[ "${INSTALL_MODE}" == "per-repo" ]]; then
114+
DEST=".fullsend/"
115+
fi
112116
for dir in ${LAYERED_DIRS}; do
113117
if [[ -d "${SRC}/${dir}" ]]; then
114-
mkdir -p "${dir}"
115-
cp -r "${SRC}/${dir}/." "${dir}/"
118+
mkdir -p "${DEST}${dir}"
119+
cp -r "${SRC}/${dir}/." "${DEST}${dir}/"
116120
fi
117121
done
118122
CUSTOM_BASE="customized"
@@ -124,8 +128,8 @@ jobs:
124128
find "${CUSTOM_BASE}/${dir}" -type f ! -name '.gitkeep' -print0 \
125129
| while IFS= read -r -d '' f; do
126130
rel="${f#"${CUSTOM_BASE}"/}"
127-
mkdir -p "$(dirname "${rel}")"
128-
cp "${f}" "${rel}"
131+
mkdir -p "$(dirname "${DEST}${rel}")"
132+
cp "${f}" "${DEST}${rel}"
129133
done
130134
fi
131135
done
@@ -396,6 +400,7 @@ jobs:
396400
with:
397401
agent: fix
398402
version: ${{ inputs.fullsend_version }}
403+
fullsend-dir: ${{ inputs.install_mode == 'per-repo' && '.fullsend' || '' }}
399404
run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
400405
status-repo: ${{ inputs.source_repo }}
401406
status-number: ${{ steps.context.outputs.pr_number }}

.github/workflows/reusable-prioritize.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,14 @@ jobs:
9494
fi
9595
SRC=".defaults/internal/scaffold/fullsend-repo"
9696
LAYERED_DIRS="agents skills schemas harness plugins policies scripts env"
97+
DEST=""
98+
if [[ "${INSTALL_MODE}" == "per-repo" ]]; then
99+
DEST=".fullsend/"
100+
fi
97101
for dir in ${LAYERED_DIRS}; do
98102
if [[ -d "${SRC}/${dir}" ]]; then
99-
mkdir -p "${dir}"
100-
cp -r "${SRC}/${dir}/." "${dir}/"
103+
mkdir -p "${DEST}${dir}"
104+
cp -r "${SRC}/${dir}/." "${DEST}${dir}/"
101105
fi
102106
done
103107
CUSTOM_BASE="customized"
@@ -109,8 +113,8 @@ jobs:
109113
find "${CUSTOM_BASE}/${dir}" -type f ! -name '.gitkeep' -print0 \
110114
| while IFS= read -r -d '' f; do
111115
rel="${f#"${CUSTOM_BASE}"/}"
112-
mkdir -p "$(dirname "${rel}")"
113-
cp "${f}" "${rel}"
116+
mkdir -p "$(dirname "${DEST}${rel}")"
117+
cp "${f}" "${DEST}${rel}"
114118
done
115119
fi
116120
done
@@ -151,4 +155,5 @@ jobs:
151155
with:
152156
agent: prioritize
153157
version: ${{ inputs.fullsend_version }}
158+
fullsend-dir: ${{ inputs.install_mode == 'per-repo' && '.fullsend' || '' }}
154159
mint-url: ${{ inputs.mint_url }}

.github/workflows/reusable-retro.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,14 @@ jobs:
9090
fi
9191
SRC=".defaults/internal/scaffold/fullsend-repo"
9292
LAYERED_DIRS="agents skills schemas harness plugins policies scripts env"
93+
DEST=""
94+
if [[ "${INSTALL_MODE}" == "per-repo" ]]; then
95+
DEST=".fullsend/"
96+
fi
9397
for dir in ${LAYERED_DIRS}; do
9498
if [[ -d "${SRC}/${dir}" ]]; then
95-
mkdir -p "${dir}"
96-
cp -r "${SRC}/${dir}/." "${dir}/"
99+
mkdir -p "${DEST}${dir}"
100+
cp -r "${SRC}/${dir}/." "${DEST}${dir}/"
97101
fi
98102
done
99103
CUSTOM_BASE="customized"
@@ -105,8 +109,8 @@ jobs:
105109
find "${CUSTOM_BASE}/${dir}" -type f ! -name '.gitkeep' -print0 \
106110
| while IFS= read -r -d '' f; do
107111
rel="${f#"${CUSTOM_BASE}"/}"
108-
mkdir -p "$(dirname "${rel}")"
109-
cp "${f}" "${rel}"
112+
mkdir -p "$(dirname "${DEST}${rel}")"
113+
cp "${f}" "${DEST}${rel}"
110114
done
111115
fi
112116
done
@@ -162,6 +166,7 @@ jobs:
162166
with:
163167
agent: retro
164168
version: ${{ inputs.fullsend_version }}
169+
fullsend-dir: ${{ inputs.install_mode == 'per-repo' && '.fullsend' || '' }}
165170
run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
166171
status-repo: ${{ inputs.source_repo }}
167172
status-number: ${{ fromJSON(inputs.event_payload).pull_request.number || fromJSON(inputs.event_payload).issue.number }}

.github/workflows/reusable-review.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,14 @@ jobs:
9191
fi
9292
SRC=".defaults/internal/scaffold/fullsend-repo"
9393
LAYERED_DIRS="agents skills schemas harness plugins policies scripts env"
94+
DEST=""
95+
if [[ "${INSTALL_MODE}" == "per-repo" ]]; then
96+
DEST=".fullsend/"
97+
fi
9498
for dir in ${LAYERED_DIRS}; do
9599
if [[ -d "${SRC}/${dir}" ]]; then
96-
mkdir -p "${dir}"
97-
cp -r "${SRC}/${dir}/." "${dir}/"
100+
mkdir -p "${DEST}${dir}"
101+
cp -r "${SRC}/${dir}/." "${DEST}${dir}/"
98102
fi
99103
done
100104
CUSTOM_BASE="customized"
@@ -106,8 +110,8 @@ jobs:
106110
find "${CUSTOM_BASE}/${dir}" -type f ! -name '.gitkeep' -print0 \
107111
| while IFS= read -r -d '' f; do
108112
rel="${f#"${CUSTOM_BASE}"/}"
109-
mkdir -p "$(dirname "${rel}")"
110-
cp "${f}" "${rel}"
113+
mkdir -p "$(dirname "${DEST}${rel}")"
114+
cp "${f}" "${DEST}${rel}"
111115
done
112116
fi
113117
done
@@ -176,6 +180,7 @@ jobs:
176180
PRIOR_REVIEW_PROVENANCE: ${{ steps.prior-review.outputs.prior_review_provenance }}
177181
with:
178182
agent: review
183+
fullsend-dir: ${{ inputs.install_mode == 'per-repo' && '.fullsend' || '' }}
179184
version: ${{ inputs.fullsend_version }}
180185
run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
181186
status-repo: ${{ inputs.source_repo }}

.github/workflows/reusable-triage.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,14 @@ jobs:
9191
fi
9292
SRC=".defaults/internal/scaffold/fullsend-repo"
9393
LAYERED_DIRS="agents skills schemas harness plugins policies scripts env"
94+
DEST=""
95+
if [[ "${INSTALL_MODE}" == "per-repo" ]]; then
96+
DEST=".fullsend/"
97+
fi
9498
for dir in ${LAYERED_DIRS}; do
9599
if [[ -d "${SRC}/${dir}" ]]; then
96-
mkdir -p "${dir}"
97-
cp -r "${SRC}/${dir}/." "${dir}/"
100+
mkdir -p "${DEST}${dir}"
101+
cp -r "${SRC}/${dir}/." "${DEST}${dir}/"
98102
fi
99103
done
100104
CUSTOM_BASE="customized"
@@ -106,8 +110,8 @@ jobs:
106110
find "${CUSTOM_BASE}/${dir}" -type f ! -name '.gitkeep' -print0 \
107111
| while IFS= read -r -d '' f; do
108112
rel="${f#"${CUSTOM_BASE}"/}"
109-
mkdir -p "$(dirname "${rel}")"
110-
cp "${f}" "${rel}"
113+
mkdir -p "$(dirname "${DEST}${rel}")"
114+
cp "${f}" "${DEST}${rel}"
111115
done
112116
fi
113117
done
@@ -161,6 +165,7 @@ jobs:
161165
with:
162166
agent: triage
163167
version: ${{ inputs.fullsend_version }}
168+
fullsend-dir: ${{ inputs.install_mode == 'per-repo' && '.fullsend' || '' }}
164169
run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
165170
status-repo: ${{ inputs.source_repo }}
166171
status-number: ${{ fromJSON(inputs.event_payload).issue.number }}

internal/cli/lock_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,7 @@ func TestRunLock_URLRefsNoOrgConfigError(t *testing.T) {
10791079
printer := ui.New(os.Stdout)
10801080
err := runLock(context.Background(), "noconfig", dir, "", false, resolveFlags{}, printer)
10811081
require.Error(t, err)
1082-
assert.Contains(t, err.Error(), "URL-referenced resources require an org-level config.yaml")
1082+
assert.Contains(t, err.Error(), "URL-referenced resources require a config.yaml")
10831083
assert.Contains(t, err.Error(), "allowed_remote_resources")
10841084
}
10851085

@@ -1136,7 +1136,7 @@ func TestRunLock_MalformedOrgConfigWithURLRefs(t *testing.T) {
11361136
printer := ui.New(os.Stdout)
11371137
err := runLock(context.Background(), "badcfg", dir, "", false, resolveFlags{}, printer)
11381138
require.Error(t, err)
1139-
assert.Contains(t, err.Error(), "parsing org config")
1139+
assert.Contains(t, err.Error(), "parsing config")
11401140
}
11411141

11421142
func TestRunLock_NoOrgConfigNoURLRefs(t *testing.T) {
@@ -1202,7 +1202,7 @@ func TestRunLock_OrgAllowlistSyncedAfterReAttempt(t *testing.T) {
12021202
printer := ui.New(os.Stdout)
12031203
err := runLock(context.Background(), "urlrefs", dir, "", false, resolveFlags{}, printer)
12041204
require.Error(t, err)
1205-
assert.Contains(t, err.Error(), "parsing org config")
1205+
assert.Contains(t, err.Error(), "parsing config")
12061206
}
12071207

12081208
func TestRunLock_URLBaseAndURLRefsNoOrgConfig(t *testing.T) {

internal/cli/orgconfig.go

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,41 +17,59 @@ func configAgentNames(agents []config.AgentEntry) []string {
1717
return names
1818
}
1919

20-
// tryLoadOrgConfig attempts to load org config from the given path.
21-
// Returns nil without error when the file is absent (best-effort).
22-
// Logs warnings via printer for non-ENOENT read errors and parse errors.
23-
func tryLoadOrgConfig(path string, printer *ui.Printer) *config.OrgConfig {
20+
// tryLoadConfig attempts to load config from the given path, accepting both
21+
// OrgConfig (per-org) and PerRepoConfig (per-repo) YAML formats. Returns nil
22+
// without error when the file is absent (best-effort). Per-repo config is
23+
// adapted to OrgConfig via OrgConfigFromPerRepo so callers see a unified type.
24+
func tryLoadConfig(path string, printer *ui.Printer) *config.OrgConfig {
2425
data, err := os.ReadFile(path)
2526
if err != nil {
2627
if !os.IsNotExist(err) {
27-
printer.StepWarn("Org config unreadable (remote resource allowlist unavailable): " + err.Error())
28+
printer.StepWarn("Config unreadable (remote resource allowlist unavailable): " + err.Error())
2829
}
2930
return nil
3031
}
3132
cfg, parseErr := config.ParseOrgConfig(data)
32-
if parseErr != nil {
33-
printer.StepWarn("Org config malformed (remote resource allowlist unavailable): " + parseErr.Error())
33+
if parseErr == nil {
34+
return cfg
35+
}
36+
perRepo, perRepoErr := config.ParsePerRepoConfig(data)
37+
if perRepoErr != nil {
38+
printer.StepWarn("Config malformed (remote resource allowlist unavailable): " + parseErr.Error())
3439
return nil
3540
}
36-
return cfg
41+
return config.OrgConfigFromPerRepo(perRepo)
3742
}
3843

39-
// requireOrgConfig loads org config from the given path with strict error
40-
// handling. Returns differentiated errors for missing files, unreadable
41-
// files, and parse failures.
42-
func requireOrgConfig(path string, printer *ui.Printer) (*config.OrgConfig, error) {
44+
// tryLoadOrgConfig is an alias for tryLoadConfig, kept for backward
45+
// compatibility with callers that use the original name.
46+
var tryLoadOrgConfig = tryLoadConfig
47+
48+
// requireConfig loads config from the given path with strict error handling.
49+
// Accepts both OrgConfig and PerRepoConfig YAML; the latter is adapted via
50+
// OrgConfigFromPerRepo. Returns differentiated errors for missing files,
51+
// unreadable files, and parse failures.
52+
func requireConfig(path string, printer *ui.Printer) (*config.OrgConfig, error) {
4353
data, err := os.ReadFile(path)
4454
if err != nil {
45-
printer.StepFail("Failed to load org config")
55+
printer.StepFail("Failed to load config")
4656
if os.IsNotExist(err) {
47-
return nil, fmt.Errorf("URL-referenced resources require an org-level config.yaml with allowed_remote_resources (expected at %s)", path)
57+
return nil, fmt.Errorf("URL-referenced resources require a config.yaml with allowed_remote_resources (expected at %s)", path)
4858
}
49-
return nil, fmt.Errorf("reading org config for remote resource validation: %w", err)
59+
return nil, fmt.Errorf("reading config for remote resource validation: %w", err)
5060
}
5161
cfg, parseErr := config.ParseOrgConfig(data)
52-
if parseErr != nil {
53-
printer.StepFail("Failed to parse org config")
54-
return nil, fmt.Errorf("parsing org config: %w", parseErr)
62+
if parseErr == nil {
63+
return cfg, nil
5564
}
56-
return cfg, nil
65+
perRepo, perRepoErr := config.ParsePerRepoConfig(data)
66+
if perRepoErr != nil {
67+
printer.StepFail("Failed to parse config")
68+
return nil, fmt.Errorf("parsing config: %w", parseErr)
69+
}
70+
return config.OrgConfigFromPerRepo(perRepo), nil
5771
}
72+
73+
// requireOrgConfig is an alias for requireConfig, kept for backward
74+
// compatibility with callers that use the original name.
75+
var requireOrgConfig = requireConfig

0 commit comments

Comments
 (0)