guardian-framework 0.1.70 → 0.1.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/extensions/architect-lib/generators.ts +6 -1
- package/templates/pi/extensions/architect-lib/helpers.ts +10 -2
- package/templates/pi/extensions/architect.ts +15 -22
- package/templates/pi/extensions/pipeline.ts +82 -1
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/scripts/create-mr.sh +1 -1
- package/templates/pi/scripts/git/close-epic.sh +27 -7
- package/templates/pi/scripts/git/close-issue.sh +16 -4
- package/templates/pi/scripts/git/link-issue-to-epic.sh +24 -16
- package/templates/pi/scripts/git/update-tracking-issue.sh +17 -3
- package/templates/pi/scripts/merge-mr.sh +1 -1
package/dist/cli.js
CHANGED
|
@@ -3197,7 +3197,7 @@ __export(exports_package, {
|
|
|
3197
3197
|
bin: () => bin,
|
|
3198
3198
|
author: () => author
|
|
3199
3199
|
});
|
|
3200
|
-
var name = "guardian-framework", version = "0.1.
|
|
3200
|
+
var name = "guardian-framework", version = "0.1.72", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
|
|
3201
3201
|
var init_package = __esm(() => {
|
|
3202
3202
|
exports = {
|
|
3203
3203
|
".": {
|
package/dist/exports.js
CHANGED
|
@@ -1982,7 +1982,7 @@ __export(exports_package, {
|
|
|
1982
1982
|
bin: () => bin,
|
|
1983
1983
|
author: () => author
|
|
1984
1984
|
});
|
|
1985
|
-
var name = "guardian-framework", version = "0.1.
|
|
1985
|
+
var name = "guardian-framework", version = "0.1.72", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
|
|
1986
1986
|
var init_package = __esm(() => {
|
|
1987
1987
|
exports = {
|
|
1988
1988
|
".": {
|
package/package.json
CHANGED
|
@@ -7,11 +7,16 @@ export function generateIssueMarkdown(
|
|
|
7
7
|
slice: ArchitectureSlice,
|
|
8
8
|
issueIndex: number,
|
|
9
9
|
totalIssues: number,
|
|
10
|
+
tdd?: boolean,
|
|
10
11
|
): string {
|
|
11
12
|
const moduleId = slice.module.replace(/^module-/, "");
|
|
12
13
|
const componentName = component.name.toLowerCase().replace(/\s+/g, "-");
|
|
13
14
|
const issueId = `ISSUE-${moduleId.toUpperCase()}-${issueIndex + 1}`;
|
|
14
15
|
|
|
16
|
+
const testLine = tdd
|
|
17
|
+
? ` - "update: tests/unit/${moduleId}/${componentName}/ (failing tests already generated — make them pass)"`
|
|
18
|
+
: ` - "create: tests/unit/${moduleId}/${componentName}/"`;
|
|
19
|
+
|
|
15
20
|
return `---
|
|
16
21
|
guardian_issue:
|
|
17
22
|
id: "${issueId}"
|
|
@@ -70,7 +75,7 @@ ${component.dependencies.map((d) => ` - "${d}"`).join("\n")}
|
|
|
70
75
|
- "modify: src/${moduleId}/application/"
|
|
71
76
|
- "modify: src/${moduleId}/infrastructure/"
|
|
72
77
|
- "modify: src/${moduleId}/interfaces/"
|
|
73
|
-
-
|
|
78
|
+
- ${testLine}
|
|
74
79
|
- "create: tests/integration/${moduleId}/${componentName}/"
|
|
75
80
|
---
|
|
76
81
|
|
|
@@ -161,8 +161,16 @@ export function ensureRemoteRepo(
|
|
|
161
161
|
`glab repo create "${repository}" --private --description "Epic: ${epicName}" 2>&1`,
|
|
162
162
|
);
|
|
163
163
|
if (createResult.exitCode !== 0) {
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// Repo may already exist — verify via API before giving up
|
|
165
|
+
const viewResult = runScript(
|
|
166
|
+
cwd,
|
|
167
|
+
`glab api "projects/${repository}" --method GET 2>/dev/null | jq -r '.id // empty'`,
|
|
168
|
+
);
|
|
169
|
+
if (viewResult.exitCode !== 0 || !viewResult.stdout.trim()) {
|
|
170
|
+
console.warn(`Failed to create or find GitLab project: ${createResult.stdout}`);
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
console.warn(`GitLab project already exists, connecting to it.`);
|
|
166
174
|
}
|
|
167
175
|
runScript(cwd, "git remote remove origin 2>/dev/null");
|
|
168
176
|
const httpsUrl = `${glabBaseUrl}/${repository}.git`;
|
|
@@ -147,6 +147,14 @@ class EpicManager {
|
|
|
147
147
|
|
|
148
148
|
ctx.ui.setStatus("architect", `Planning epic: ${name}`);
|
|
149
149
|
|
|
150
|
+
// Ensure git repo is initialized before any remote operations
|
|
151
|
+
const gitCheck = runScript(this.cwd, "git rev-parse --git-dir 2>/dev/null");
|
|
152
|
+
if (gitCheck.exitCode !== 0) {
|
|
153
|
+
runScript(this.cwd, "git init");
|
|
154
|
+
runScript(this.cwd, "git add .");
|
|
155
|
+
runScript(this.cwd, 'git commit -m "Initial Guardian scaffold"');
|
|
156
|
+
}
|
|
157
|
+
|
|
150
158
|
const repoTool = readRepoTool(this.cwd);
|
|
151
159
|
const repository = readRepository(this.cwd);
|
|
152
160
|
const targetRepo = repository || slice.module;
|
|
@@ -159,18 +167,11 @@ class EpicManager {
|
|
|
159
167
|
repoTool === "glab" ? "glab auth status 2>/dev/null" : "gh auth status 2>/dev/null",
|
|
160
168
|
);
|
|
161
169
|
if (authCheck.exitCode === 0) {
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
: `gh api "repos/${repository}" --method GET 2>/dev/null | jq -r '.id // empty'`,
|
|
168
|
-
) : { exitCode: 0, stdout: "" };
|
|
169
|
-
|
|
170
|
-
if (projectAccessCheck.exitCode === 0 && projectAccessCheck.stdout.trim().length > 0) {
|
|
171
|
-
remoteRepo = ensureRemoteRepo(this.cwd, targetRepo, name, repoTool);
|
|
172
|
-
hasRemote = remoteRepo.length > 0;
|
|
173
|
-
}
|
|
170
|
+
// Try to ensure remote repo — ensureRemoteRepo handles both:
|
|
171
|
+
// - existing repos: configures local origin
|
|
172
|
+
// - nonexistent repos: creates remote repo via gh/glab CLI
|
|
173
|
+
remoteRepo = ensureRemoteRepo(this.cwd, targetRepo, name, repoTool);
|
|
174
|
+
hasRemote = remoteRepo.length > 0;
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -250,7 +251,7 @@ class EpicManager {
|
|
|
250
251
|
status: "planned" as string,
|
|
251
252
|
remoteIssueId: null as string | null,
|
|
252
253
|
};
|
|
253
|
-
const md = generateIssueMarkdown(comp, slice, i, slice.nextLogicalSlice.length);
|
|
254
|
+
const md = generateIssueMarkdown(comp, slice, i, slice.nextLogicalSlice.length, tdd);
|
|
254
255
|
writeFileSync(join(issuesDir, `${id}.md`), md);
|
|
255
256
|
if (hasRemote && remoteRepo) {
|
|
256
257
|
const result = createRemoteIssue(this.cwd, entry.title, join(issuesDir, `${id}.md`), "epic,implementation", remoteRepo);
|
|
@@ -757,15 +758,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
757
758
|
return;
|
|
758
759
|
}
|
|
759
760
|
|
|
760
|
-
|
|
761
|
-
try {
|
|
762
|
-
const gitCheck = runScript(ctx.cwd, "git rev-parse --git-dir 2>/dev/null");
|
|
763
|
-
if (gitCheck.exitCode !== 0) {
|
|
764
|
-
runScript(ctx.cwd, "git init");
|
|
765
|
-
runScript(ctx.cwd, "git add .");
|
|
766
|
-
runScript(ctx.cwd, 'git commit -m "Initial Guardian scaffold"');
|
|
767
|
-
}
|
|
768
|
-
} catch { /* ignore */ }
|
|
761
|
+
|
|
769
762
|
|
|
770
763
|
// Remove stale pipeline state so the new one takes effect
|
|
771
764
|
try {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* /pipeline abort Kill pipeline
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { execSync } from "node:child_process";
|
|
23
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
24
24
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
25
25
|
import { dirname, join } from "node:path";
|
|
26
26
|
import {
|
|
@@ -863,14 +863,71 @@ export default function (pi: ExtensionAPI) {
|
|
|
863
863
|
const prevItemIndex = state.currentItemIndex;
|
|
864
864
|
const prevStepIndex = state.currentStepIndex;
|
|
865
865
|
const stepName = (params.stepName as string) || state.steps[prevStepIndex]?.name;
|
|
866
|
+
|
|
867
|
+
// Verify current step before advancing (unless user explicitly provides the step name,
|
|
868
|
+
// which counts as manual confirmation)
|
|
869
|
+
if (!params.stepName) {
|
|
870
|
+
const verification = manager.verifyCurrentStep();
|
|
871
|
+
if (!verification.verified) {
|
|
872
|
+
return {
|
|
873
|
+
content: [{ type: "text" as const, text: `Cannot advance: ${verification.reason}` }],
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
866
878
|
manager.markStepPassed(stepName);
|
|
867
879
|
manager.advanceStep();
|
|
868
880
|
|
|
869
881
|
// Re-read state after advance
|
|
870
882
|
const updatedState = manager.getState()!;
|
|
871
883
|
|
|
884
|
+
// Non-blocking: sync progress to tracking issue if one exists
|
|
885
|
+
try {
|
|
886
|
+
const trackingStatePath = join(ctx.cwd, ".pi/.guardian-epic-state.json");
|
|
887
|
+
if (existsSync(trackingStatePath)) {
|
|
888
|
+
const trackingState = JSON.parse(readFileSync(trackingStatePath, "utf-8")) as {
|
|
889
|
+
trackingIssueId?: string | null;
|
|
890
|
+
issues?: { id: string; title: string; status: string }[];
|
|
891
|
+
};
|
|
892
|
+
if (trackingState.trackingIssueId && !trackingState.trackingIssueId.startsWith("local:")) {
|
|
893
|
+
const stepNote = stepName
|
|
894
|
+
? `✓ Step "${stepName}" complete for item "${updatedState.items[prevItemIndex]}"`
|
|
895
|
+
: `✓ Advanced to item ${updatedState.currentItemIndex + 1}/${updatedState.items.length}: "${updatedState.items[updatedState.currentItemIndex]}"`;
|
|
896
|
+
const updateScript = join(ctx.cwd, ".pi/scripts/git/update-tracking-issue.sh");
|
|
897
|
+
if (existsSync(updateScript)) {
|
|
898
|
+
execFileSync("bash", [updateScript, "--id", trackingState.trackingIssueId, "--comment", stepNote], {
|
|
899
|
+
cwd: ctx.cwd,
|
|
900
|
+
timeout: 30_000,
|
|
901
|
+
encoding: "utf-8",
|
|
902
|
+
stdio: "ignore",
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} catch { /* non-blocking — don't fail the pipeline */ }
|
|
908
|
+
|
|
872
909
|
// Pipeline complete
|
|
873
910
|
if (updatedState.currentItemIndex >= updatedState.items.length) {
|
|
911
|
+
// Non-blocking: close tracking issue and epic on pipeline completion
|
|
912
|
+
try {
|
|
913
|
+
const epicStatePath = join(ctx.cwd, ".pi/.guardian-epic-state.json");
|
|
914
|
+
if (existsSync(epicStatePath)) {
|
|
915
|
+
const epicState = JSON.parse(readFileSync(epicStatePath, "utf-8")) as {
|
|
916
|
+
trackingIssueId?: string | null;
|
|
917
|
+
epicId?: string | null;
|
|
918
|
+
};
|
|
919
|
+
if (epicState.trackingIssueId && !epicState.trackingIssueId.startsWith("local:")) {
|
|
920
|
+
const closeEpicScript = join(ctx.cwd, ".pi/scripts/git/close-epic.sh");
|
|
921
|
+
if (existsSync(closeEpicScript)) {
|
|
922
|
+
const args: string[] = [closeEpicScript, "--tracking-id", epicState.trackingIssueId];
|
|
923
|
+
if (epicState.epicId) args.push("--epic-id", epicState.epicId);
|
|
924
|
+
execFileSync("bash", args, {
|
|
925
|
+
cwd: ctx.cwd, timeout: 30_000, encoding: "utf-8", stdio: "ignore",
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
} catch { /* non-blocking */ }
|
|
874
931
|
return {
|
|
875
932
|
content: [{
|
|
876
933
|
type: "text" as const,
|
|
@@ -884,6 +941,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
884
941
|
const movedToNextItem = updatedState.currentItemIndex !== prevItemIndex;
|
|
885
942
|
|
|
886
943
|
// If we moved to a new item (completed all steps of previous item),
|
|
944
|
+
// close its remote issue
|
|
945
|
+
if (movedToNextItem) {
|
|
946
|
+
// Close remote issue for the completed item
|
|
947
|
+
try {
|
|
948
|
+
const epicStatePath = join(ctx.cwd, ".pi/.guardian-epic-state.json");
|
|
949
|
+
if (existsSync(epicStatePath)) {
|
|
950
|
+
const epicState = JSON.parse(readFileSync(epicStatePath, "utf-8")) as {
|
|
951
|
+
issues?: { id: string; remoteIssueId?: string | null }[];
|
|
952
|
+
};
|
|
953
|
+
const prevItemId = updatedState.items[prevItemIndex];
|
|
954
|
+
const prevIssue = epicState.issues?.find((i) => i.id === prevItemId);
|
|
955
|
+
if (prevIssue?.remoteIssueId) {
|
|
956
|
+
const closeScript = join(ctx.cwd, ".pi/scripts/git/close-issue.sh");
|
|
957
|
+
if (existsSync(closeScript)) {
|
|
958
|
+
execFileSync("bash", [closeScript, "--id", prevIssue.remoteIssueId], {
|
|
959
|
+
cwd: ctx.cwd, timeout: 30_000, encoding: "utf-8", stdio: "ignore",
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
} catch { /* non-blocking */ }
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// If we moved to a new item and the next step is implement,
|
|
887
968
|
// inject the full next-task prompt with issue context
|
|
888
969
|
if (movedToNextItem && currentStep?.name === "implement") {
|
|
889
970
|
// Find the remote issue ID from epic state
|
|
@@ -56,7 +56,7 @@ if [[ "$PLATFORM" == "none" ]]; then
|
|
|
56
56
|
fi
|
|
57
57
|
|
|
58
58
|
# Determine branch name from issue ID
|
|
59
|
-
BRANCH_NAME="
|
|
59
|
+
BRANCH_NAME="feat/${ISSUE_ID}"
|
|
60
60
|
|
|
61
61
|
# Ensure we're on the main branch before creating feature branch
|
|
62
62
|
git checkout -b "$BRANCH_NAME" 2>/dev/null || git checkout "$BRANCH_NAME" 2>/dev/null || true
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
# Usage: bash .pi/scripts/git/close-epic.sh \
|
|
7
7
|
# --epic-id 101 \
|
|
8
8
|
# --tracking-id 100 \
|
|
9
|
-
# --comment "Epic complete. All 5 issues done."
|
|
9
|
+
# --comment "Epic complete. All 5 issues done." \
|
|
10
|
+
# [--repo "group/project"]
|
|
10
11
|
|
|
11
12
|
set -euo pipefail
|
|
12
13
|
|
|
@@ -22,44 +23,63 @@ detect_platform() {
|
|
|
22
23
|
else echo "none"; fi
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
read_repository() {
|
|
27
|
+
if [ -f "guardian-manifest.json" ]; then
|
|
28
|
+
jq -r '.repository // (.templateContext.repository // "")' guardian-manifest.json 2>/dev/null || echo ""
|
|
29
|
+
else
|
|
30
|
+
echo ""
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
EPIC_ID=""
|
|
26
35
|
TRACKING_ID=""
|
|
27
36
|
COMMENT="Epic complete."
|
|
37
|
+
REPO=""
|
|
28
38
|
|
|
29
39
|
while [[ $# -gt 0 ]]; do
|
|
30
40
|
case $1 in
|
|
31
41
|
--epic-id) EPIC_ID="$2"; shift 2 ;;
|
|
32
42
|
--tracking-id) TRACKING_ID="$2"; shift 2 ;;
|
|
33
43
|
--comment) COMMENT="$2"; shift 2 ;;
|
|
44
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
34
45
|
*) shift ;;
|
|
35
46
|
esac
|
|
36
47
|
done
|
|
37
48
|
|
|
38
49
|
PLATFORM=$(detect_platform)
|
|
50
|
+
[[ -z "$REPO" ]] && REPO=$(read_repository)
|
|
39
51
|
|
|
40
52
|
if [[ -z "$EPIC_ID" ]]; then
|
|
41
|
-
echo "Usage: $0 --epic-id <epic_number> [--tracking-id <tracking_id>] [--comment <comment>]"
|
|
53
|
+
echo "Usage: $0 --epic-id <epic_number> [--tracking-id <tracking_id>] [--comment <comment>] [--repo <repo>]"
|
|
42
54
|
exit 1
|
|
43
55
|
fi
|
|
44
56
|
|
|
45
57
|
# Close tracking issue first
|
|
46
58
|
if [[ -n "$TRACKING_ID" ]]; then
|
|
47
|
-
bash "$(dirname "$0")/close-issue.sh" --id "$TRACKING_ID"
|
|
59
|
+
bash "$(dirname "$0")/close-issue.sh" --id "$TRACKING_ID" ${REPO:+--repo "$REPO"}
|
|
48
60
|
fi
|
|
49
61
|
|
|
50
62
|
# Close the epic
|
|
51
63
|
case "$PLATFORM" in
|
|
52
64
|
github)
|
|
53
|
-
gh issue close "$EPIC_ID" 2>/dev/null
|
|
65
|
+
gh issue close "$EPIC_ID" ${REPO:+--repo "$REPO"} 2>/dev/null
|
|
54
66
|
if [[ -n "$COMMENT" ]]; then
|
|
55
|
-
gh issue comment "$EPIC_ID" --body "$COMMENT" 2>/dev/null
|
|
67
|
+
gh issue comment "$EPIC_ID" ${REPO:+--repo "$REPO"} --body "$COMMENT" 2>/dev/null
|
|
56
68
|
fi
|
|
57
69
|
echo "Closed GitHub epic #$EPIC_ID"
|
|
58
70
|
;;
|
|
59
71
|
gitlab)
|
|
60
|
-
|
|
72
|
+
# If EPIC_ID looks numeric, use the GitLab Epic API
|
|
73
|
+
if [[ "$EPIC_ID" =~ ^[0-9]+$ ]]; then
|
|
74
|
+
# Try epic close first, fallback to issue close
|
|
75
|
+
glab api --method PUT "epics/$EPIC_ID" --body '{"state_event":"close"}' 2>/dev/null && \
|
|
76
|
+
echo "Closed GitLab epic #$EPIC_ID" || \
|
|
77
|
+
glab issue update "$EPIC_ID" --state-event close ${REPO:+--repo "$REPO"} 2>/dev/null
|
|
78
|
+
else
|
|
79
|
+
glab issue update "$EPIC_ID" --state-event close ${REPO:+--repo "$REPO"} 2>/dev/null
|
|
80
|
+
fi
|
|
61
81
|
if [[ -n "$COMMENT" ]]; then
|
|
62
|
-
glab issue note "$EPIC_ID" --message "$COMMENT" 2>/dev/null
|
|
82
|
+
glab issue note "$EPIC_ID" ${REPO:+--repo "$REPO"} --message "$COMMENT" 2>/dev/null || true
|
|
63
83
|
fi
|
|
64
84
|
echo "Closed GitLab epic #$EPIC_ID"
|
|
65
85
|
;;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Git Wrapper: Close Issue
|
|
3
3
|
#
|
|
4
|
-
# Usage: bash .pi/scripts/git/close-issue.sh --id 101
|
|
4
|
+
# Usage: bash .pi/scripts/git/close-issue.sh --id 101 [--repo "group/project"]
|
|
5
5
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
@@ -17,19 +17,31 @@ detect_platform() {
|
|
|
17
17
|
else echo "none"; fi
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
read_repository() {
|
|
21
|
+
if [ -f "guardian-manifest.json" ]; then
|
|
22
|
+
jq -r '.repository // (.templateContext.repository // "")' guardian-manifest.json 2>/dev/null || echo ""
|
|
23
|
+
else
|
|
24
|
+
echo ""
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
ISSUE_ID=""
|
|
29
|
+
REPO=""
|
|
30
|
+
|
|
21
31
|
while [[ $# -gt 0 ]]; do
|
|
22
32
|
case $1 in
|
|
23
33
|
--id) ISSUE_ID="$2"; shift 2 ;;
|
|
34
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
24
35
|
*) shift ;;
|
|
25
36
|
esac
|
|
26
37
|
done
|
|
27
38
|
|
|
28
|
-
[[ -z "$ISSUE_ID" ]] && { echo "Usage: $0 --id <issue_number>"; exit 1; }
|
|
39
|
+
[[ -z "$ISSUE_ID" ]] && { echo "Usage: $0 --id <issue_number> [--repo <repo>]"; exit 1; }
|
|
29
40
|
PLATFORM=$(detect_platform)
|
|
41
|
+
[[ -z "$REPO" ]] && REPO=$(read_repository)
|
|
30
42
|
|
|
31
43
|
case "$PLATFORM" in
|
|
32
|
-
github) gh issue close "$ISSUE_ID" 2>/dev/null && echo "Closed GitHub issue #$ISSUE_ID" ;;
|
|
33
|
-
gitlab) glab issue update "$ISSUE_ID" --state-event close 2>/dev/null && echo "Closed GitLab issue #$ISSUE_ID" ;;
|
|
44
|
+
github) gh issue close "$ISSUE_ID" ${REPO:+--repo "$REPO"} 2>/dev/null && echo "Closed GitHub issue #$ISSUE_ID" ;;
|
|
45
|
+
gitlab) glab issue update "$ISSUE_ID" --state-event close ${REPO:+--repo "$REPO"} 2>/dev/null && echo "Closed GitLab issue #$ISSUE_ID" ;;
|
|
34
46
|
*) echo "Local issue #$ISSUE_ID marked as closed (no platform)" ;;
|
|
35
47
|
esac
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Usage: bash .pi/scripts/git/link-issue-to-epic.sh \
|
|
5
5
|
# --issue-id 102 \
|
|
6
|
-
# --epic-id 101
|
|
6
|
+
# --epic-id 101 \
|
|
7
|
+
# [--repo "group/project"]
|
|
7
8
|
|
|
8
9
|
set -euo pipefail
|
|
9
10
|
|
|
@@ -19,46 +20,53 @@ detect_platform() {
|
|
|
19
20
|
else echo "none"; fi
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
read_repository() {
|
|
24
|
+
if [ -f "guardian-manifest.json" ]; then
|
|
25
|
+
jq -r '.repository // (.templateContext.repository // "")' guardian-manifest.json 2>/dev/null || echo ""
|
|
26
|
+
else
|
|
27
|
+
echo ""
|
|
28
|
+
fi
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
ISSUE_ID=""
|
|
23
32
|
EPIC_ID=""
|
|
33
|
+
REPO=""
|
|
24
34
|
|
|
25
35
|
while [[ $# -gt 0 ]]; do
|
|
26
36
|
case $1 in
|
|
27
37
|
--issue-id) ISSUE_ID="$2"; shift 2 ;;
|
|
28
38
|
--epic-id) EPIC_ID="$2"; shift 2 ;;
|
|
39
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
29
40
|
*) shift ;;
|
|
30
41
|
esac
|
|
31
42
|
done
|
|
32
43
|
|
|
33
|
-
[[ -z "$ISSUE_ID" || -z "$EPIC_ID" ]] && { echo "Usage: $0 --issue-id <issue> --epic-id <epic>"; exit 1; }
|
|
44
|
+
[[ -z "$ISSUE_ID" || -z "$EPIC_ID" ]] && { echo "Usage: $0 --issue-id <issue> --epic-id <epic> [--repo <repo>]"; exit 1; }
|
|
34
45
|
PLATFORM=$(detect_platform)
|
|
46
|
+
[[ -z "$REPO" ]] && REPO=$(read_repository)
|
|
35
47
|
|
|
36
48
|
case "$PLATFORM" in
|
|
37
49
|
github)
|
|
38
|
-
|
|
39
|
-
gh issue edit "$ISSUE_ID" --body "$(gh issue view "$ISSUE_ID" --json body -q .body)
|
|
50
|
+
gh issue edit "$ISSUE_ID" ${REPO:+--repo "$REPO"} --body "$(gh issue view "$ISSUE_ID" --json body -q .body 2>/dev/null || true)
|
|
40
51
|
|
|
41
52
|
Relates to #$EPIC_ID" 2>/dev/null
|
|
42
53
|
echo "Linked GitHub issue #$ISSUE_ID to #$EPIC_ID"
|
|
43
54
|
;;
|
|
44
55
|
gitlab)
|
|
45
|
-
#
|
|
56
|
+
# GitLab: link issue to epic via API
|
|
46
57
|
# API: POST /groups/:group/epics/:epic_iid/issues/:issue_iid
|
|
47
|
-
#
|
|
48
|
-
if
|
|
49
|
-
local
|
|
50
|
-
glab api "projects/$pid" --method GET 2>/dev/null | jq -r '.namespace.full_path // empty' | read -r group_path
|
|
58
|
+
# Derive group from repository path: "group/subgroup/project" -> "group/subgroup"
|
|
59
|
+
if [[ -n "$REPO" ]]; then
|
|
60
|
+
local group_path=$(echo "$REPO" | rev | cut -d'/' -f2- | rev)
|
|
51
61
|
if [[ -n "$group_path" ]]; then
|
|
52
|
-
glab api --method POST "groups/$group_path/epics/$EPIC_ID/issues/$ISSUE_ID" 2>/dev/null
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
glab api --method POST "groups/$group_path/epics/$EPIC_ID/issues/$ISSUE_ID" 2>/dev/null && {
|
|
63
|
+
echo "Linked GitLab issue #$ISSUE_ID to epic #$EPIC_ID"
|
|
64
|
+
exit 0
|
|
55
65
|
}
|
|
56
|
-
else
|
|
57
|
-
glab issue note "$ISSUE_ID" --message "Part of epic #$EPIC_ID" 2>/dev/null || true
|
|
58
66
|
fi
|
|
59
|
-
else
|
|
60
|
-
glab issue note "$ISSUE_ID" --message "Part of epic #$EPIC_ID" 2>/dev/null || true
|
|
61
67
|
fi
|
|
68
|
+
# Fallback: add a note referencing the epic
|
|
69
|
+
glab issue note "$ISSUE_ID" ${REPO:+--repo "$REPO"} --message "Part of epic #$EPIC_ID" 2>/dev/null || true
|
|
62
70
|
echo "Linked GitLab issue #$ISSUE_ID to epic #$EPIC_ID"
|
|
63
71
|
;;
|
|
64
72
|
*)
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# Usage: bash .pi/scripts/git/update-tracking-issue.sh \
|
|
7
7
|
# --id 100 \
|
|
8
8
|
# --comment "✓ Issue #102 complete (CI green, tests pass)"
|
|
9
|
+
# [--repo "group/project"]
|
|
9
10
|
|
|
10
11
|
set -euo pipefail
|
|
11
12
|
|
|
@@ -21,21 +22,34 @@ detect_platform() {
|
|
|
21
22
|
else echo "none"; fi
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
read_repository() {
|
|
26
|
+
if [ -f "guardian-manifest.json" ]; then
|
|
27
|
+
jq -r '.repository // (.templateContext.repository // "")' guardian-manifest.json 2>/dev/null || echo ""
|
|
28
|
+
else
|
|
29
|
+
echo ""
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
ISSUE_ID=""
|
|
25
34
|
COMMENT=""
|
|
35
|
+
REPO=""
|
|
26
36
|
|
|
27
37
|
while [[ $# -gt 0 ]]; do
|
|
28
38
|
case $1 in
|
|
29
39
|
--id) ISSUE_ID="$2"; shift 2 ;;
|
|
30
40
|
--comment) COMMENT="$2"; shift 2 ;;
|
|
41
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
31
42
|
*) shift ;;
|
|
32
43
|
esac
|
|
33
44
|
done
|
|
34
45
|
|
|
35
46
|
PLATFORM=$(detect_platform)
|
|
47
|
+
if [[ -z "$REPO" ]]; then
|
|
48
|
+
REPO=$(read_repository)
|
|
49
|
+
fi
|
|
36
50
|
|
|
37
51
|
if [[ -z "$ISSUE_ID" || -z "$COMMENT" ]]; then
|
|
38
|
-
echo "Usage: $0 --id <issue_number> --comment <comment_text>"
|
|
52
|
+
echo "Usage: $0 --id <issue_number> --comment <comment_text> [--repo <repo>]"
|
|
39
53
|
exit 1
|
|
40
54
|
fi
|
|
41
55
|
|
|
@@ -53,10 +67,10 @@ fi
|
|
|
53
67
|
|
|
54
68
|
case "$PLATFORM" in
|
|
55
69
|
github)
|
|
56
|
-
gh issue comment "$ISSUE_ID" --body "$COMMENT" 2>/dev/null && echo "Comment posted to GitHub issue #$ISSUE_ID"
|
|
70
|
+
gh issue comment "$ISSUE_ID" ${REPO:+--repo "$REPO"} --body "$COMMENT" 2>/dev/null && echo "Comment posted to GitHub issue #$ISSUE_ID"
|
|
57
71
|
;;
|
|
58
72
|
gitlab)
|
|
59
|
-
glab issue note "$ISSUE_ID" --message "$COMMENT" 2>/dev/null && echo "Comment posted to GitLab issue #$ISSUE_ID"
|
|
73
|
+
glab issue note "$ISSUE_ID" ${REPO:+--repo "$REPO"} --message "$COMMENT" 2>/dev/null && echo "Comment posted to GitLab issue #$ISSUE_ID"
|
|
60
74
|
;;
|
|
61
75
|
none)
|
|
62
76
|
echo "No git platform detected. Comment not posted."
|