Skip to content

Commit 8353d56

Browse files
feat: add GitHub tracking setup script for issue and discussion management
1 parent 87963c2 commit 8353d56

File tree

1 file changed

+351
-0
lines changed

1 file changed

+351
-0
lines changed

tools/setup-github-tracking.ps1

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
$ErrorActionPreference = "Stop"
2+
Set-StrictMode -Version Latest
3+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
4+
$OutputEncoding = [System.Text.Encoding]::UTF8
5+
6+
function Require-GhAuth {
7+
& gh auth status | Out-Null
8+
if ($LASTEXITCODE -ne 0) {
9+
Write-Error "GitHub CLI is not authenticated. Run: gh auth login"
10+
exit 1
11+
}
12+
}
13+
14+
function Get-RepoRoot {
15+
$root = (& git rev-parse --show-toplevel).Trim()
16+
if (-not $root) {
17+
throw "Unable to determine repo root."
18+
}
19+
return $root
20+
}
21+
22+
function Parse-OwnerRepo {
23+
param([string]$RemoteUrl)
24+
$httpsMatch = [regex]::Match($RemoteUrl, '^https://github\.com/([^/]+)/([^/]+?)(\.git)?$')
25+
if ($httpsMatch.Success) {
26+
return @($httpsMatch.Groups[1].Value, $httpsMatch.Groups[2].Value)
27+
}
28+
$sshMatch = [regex]::Match($RemoteUrl, '^git@github\.com:([^/]+)/([^/]+?)(\.git)?$')
29+
if ($sshMatch.Success) {
30+
return @($sshMatch.Groups[1].Value, $sshMatch.Groups[2].Value)
31+
}
32+
throw "Unsupported origin remote URL: $RemoteUrl"
33+
}
34+
35+
function Get-IssueByTitle {
36+
param([string]$Title)
37+
$search = "in:title $Title"
38+
$raw = & gh issue list --state all --search $search --limit 200 --json title,number,url
39+
$issues = $raw | ConvertFrom-Json
40+
return $issues | Where-Object { $_.title -eq $Title } | Select-Object -First 1
41+
}
42+
43+
function Ensure-Issue {
44+
param(
45+
[string]$Title,
46+
[string]$Body,
47+
[string[]]$Labels,
48+
[string]$Milestone
49+
)
50+
$issue = Get-IssueByTitle -Title $Title
51+
if (-not $issue) {
52+
$createArgs = @("issue", "create", "--title", $Title, "--body", $Body)
53+
if ($Labels -and $Labels.Count -gt 0) {
54+
$createArgs += @("--label", ($Labels -join ","))
55+
}
56+
if ($Milestone) {
57+
$createArgs += @("--milestone", $Milestone)
58+
}
59+
& gh @createArgs | Out-Null
60+
$issue = Get-IssueByTitle -Title $Title
61+
}
62+
if (-not $issue) {
63+
throw "Failed to create or locate issue: $Title"
64+
}
65+
$editArgs = @("issue", "edit", $issue.number)
66+
$hasEdits = $false
67+
if ($Labels -and $Labels.Count -gt 0) {
68+
$editArgs += @("--add-label", ($Labels -join ","))
69+
$hasEdits = $true
70+
}
71+
if ($Milestone) {
72+
$editArgs += @("--milestone", $Milestone)
73+
$hasEdits = $true
74+
}
75+
if ($hasEdits) {
76+
& gh @editArgs | Out-Null
77+
}
78+
return $issue
79+
}
80+
81+
Require-GhAuth
82+
$repoRoot = Get-RepoRoot
83+
Set-Location $repoRoot
84+
85+
$originUrl = (& git remote get-url origin).Trim()
86+
$parsed = Parse-OwnerRepo -RemoteUrl $originUrl
87+
$owner = $parsed[0]
88+
$repo = $parsed[1]
89+
$env:OWNER = $owner
90+
$env:REPO = $repo
91+
Write-Host "OWNER=$owner"
92+
Write-Host "REPO=$repo"
93+
94+
$enDash = [char]0x2013
95+
$leftQuote = [char]0x201c
96+
$rightQuote = [char]0x201d
97+
$apostrophe = [char]0x2019
98+
$rightArrow = [char]0x2192
99+
100+
$DISCUSSION_TITLE = "CloudSQLCTL $enDash Production Hardening & Roadmap"
101+
$ROADMAP_ISSUE_TITLE = "Roadmap: Production Finalization (P0/P1/P2) $enDash CloudSQLCTL"
102+
103+
$DISCUSSION_BODY = (@'
104+
---
105+
# CloudSQLCTL {0} Production Hardening & Roadmap
106+
107+
## Goal
108+
Elevate CloudSQLCTL to a company-grade {1}production final{2} standard:
109+
- Deterministic GitHub Releases (installer + exe + zip + checksums)
110+
- Fully automated self-upgrade (`cloudsqlctl upgrade` default: check {3} download {3} verify {3} install)
111+
- NPM publish (node CLI distribution) integrated with release versioning
112+
- Secure machine-scope installation (no user-writable service binaries)
113+
- Supportability (support bundle, structured logs, docs automation)
114+
115+
## Current state (high-level)
116+
- Release pipeline builds SEA exe, builds Inno Setup installer, stages artifacts, uploads to GitHub Release.
117+
- Upgrade checks GitHub Releases and verifies SHA256SUMS before applying installer/portable update.
118+
- Setup supports gcloud login + ADC + instance selection.
119+
120+
## Risks / What{4}s wrong today
121+
1) **Machine-scope security**: ProgramData bin permissions must not allow Users write (service privilege escalation risk).
122+
2) **Release republish**: Re-running the same tag should update assets reliably (delete/replace clashing asset names).
123+
3) **Workflow rerun**: Manual `workflow_dispatch` is required for deterministic republish without tag gymnastics.
124+
4) **System-scope update guardrails**: system-scope writes require admin/elevation and must be enforced.
125+
5) **Supportability gap**: need one-command support bundle zip (logs/config/doctor/paths/status/gcloud info).
126+
6) **Upgrade rollout controls**: channel (stable/beta), pinned version, target version install, rollback.
127+
7) **Supply chain**: code signing + better release verification is needed for enterprise environments.
128+
8) **NPM publish**: define what we publish (node CLI) and keep contents minimal + version-gated.
129+
130+
## Milestones
131+
- P0 {0} Release & Security Blockers
132+
- P1 {0} Operations & Supportability
133+
- P2 {0} Enterprise Distribution & Signing
134+
135+
## How we will track
136+
- A single Roadmap issue will link all work items and milestones.
137+
- Each item is an issue with labels: Priority (P0/P1/P2), Area, Type.
138+
---
139+
'@ -f $enDash, $leftQuote, $rightQuote, $rightArrow, $apostrophe)
140+
141+
$ROADMAP_BODY_TEMPLATE = (@'
142+
---
143+
# Roadmap: Production Finalization (P0/P1/P2) {0} CloudSQLCTL
144+
145+
This issue tracks all work required to reach {1}production final{2}.
146+
Checklist items below link to the corresponding issues.
147+
148+
## P0 {0} Release & Security Blockers
149+
- [ ] P0: Fix Machine-scope security for ProgramData bin (prevent Users write)
150+
- [ ] P0: Release republish same tag (clobber existing assets safely)
151+
- [ ] P0: Add workflow_dispatch to release workflow + manual rerun support
152+
- [ ] P0: Update/Upgrade guardrails for system scope (admin/elevation required)
153+
- [ ] P0: Repo hygiene (.gitignore) to prevent committing bin/dist/artifacts
154+
155+
## P1 {0} Operations & Supportability
156+
- [ ] P1: Support bundle command (zip logs+config+doctor+paths+status)
157+
- [ ] P1: Upgrade channels (stable/beta) + pinned version + target version install
158+
- [ ] P1: GitHub API hardening (token support, retries, rate-limit messaging)
159+
- [ ] P1: Proxy checksum verification robustness (deterministic source)
160+
- [ ] P1: Improve portable upgrade swap (temp + atomic replace + rollback)
161+
- [ ] P1: NPM publish pipeline + package contents control
162+
163+
## P2 {0} Enterprise Distribution & Signing
164+
- [ ] P2: Code signing for exe + installer (CI integration)
165+
- [ ] P2: winget/choco/scoop distribution
166+
- [ ] P2: Enterprise policy.json (updates/auth constraints, rollout control)
167+
- [ ] P2: Service-aware upgrade coordination (stop/start service safely)
168+
169+
## Notes
170+
- Use labels: Priority:P0/P1/P2, Area:*, Type:*.
171+
- Each milestone should have a clear scope and DoD.
172+
---
173+
'@ -f $enDash, $leftQuote, $rightQuote)
174+
175+
$labelDefinitions = @(
176+
@{ Name = "P0"; Color = "ff0000" },
177+
@{ Name = "P1"; Color = "ff8c00" },
178+
@{ Name = "P2"; Color = "0066ff" },
179+
@{ Name = "bug"; Color = "b60205" },
180+
@{ Name = "enhancement"; Color = "0e8a16" },
181+
@{ Name = "chore"; Color = "c5def5" },
182+
@{ Name = "docs"; Color = "1d76db" },
183+
@{ Name = "release"; Color = "5319e7" },
184+
@{ Name = "security"; Color = "8b0000" },
185+
@{ Name = "upgrade"; Color = "0052cc" },
186+
@{ Name = "installer"; Color = "fbca04" },
187+
@{ Name = "service"; Color = "006b75" },
188+
@{ Name = "auth"; Color = "7f8c8d" },
189+
@{ Name = "npm"; Color = "e99695" },
190+
@{ Name = "blocked"; Color = "000000" },
191+
@{ Name = "ready"; Color = "2ecc71" }
192+
)
193+
194+
foreach ($label in $labelDefinitions) {
195+
& gh label create $label.Name --color $label.Color --force | Out-Null
196+
}
197+
198+
$milestoneP0 = "P0 $enDash Release & Security Blockers"
199+
$milestoneP1 = "P1 $enDash Operations & Supportability"
200+
$milestoneP2 = "P2 $enDash Enterprise Distribution & Signing"
201+
$milestoneTitles = @($milestoneP0, $milestoneP1, $milestoneP2)
202+
203+
$milestoneMap = @{}
204+
$existingMilestones = & gh api "repos/$owner/$repo/milestones?state=all&per_page=100" | ConvertFrom-Json
205+
foreach ($milestone in $existingMilestones) {
206+
$milestoneMap[$milestone.title] = [int]$milestone.number
207+
}
208+
209+
foreach ($title in $milestoneTitles) {
210+
if (-not $milestoneMap.ContainsKey($title)) {
211+
$created = & gh api -X POST "repos/$owner/$repo/milestones" -f title="$title" | ConvertFrom-Json
212+
$milestoneMap[$title] = [int]$created.number
213+
}
214+
}
215+
216+
$discussionUrl = $null
217+
try {
218+
$repoQuery = @'
219+
query($owner: String!, $name: String!) {
220+
repository(owner: $owner, name: $name) {
221+
id
222+
discussionCategories(first: 100) {
223+
nodes { id name }
224+
}
225+
discussions(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {
226+
nodes { id title url }
227+
}
228+
}
229+
}
230+
'@
231+
$repoData = & gh api graphql -f query="$repoQuery" -f owner="$owner" -f name="$repo" | ConvertFrom-Json
232+
$repoInfo = $repoData.data.repository
233+
if (-not $repoInfo) {
234+
throw "Repository not found."
235+
}
236+
$categories = $repoInfo.discussionCategories.nodes
237+
if (-not $categories -or $categories.Count -eq 0) {
238+
throw "No discussion categories found."
239+
}
240+
$category = $categories | Where-Object { $_.name -eq "General" } | Select-Object -First 1
241+
if (-not $category) {
242+
$category = $categories | Select-Object -First 1
243+
}
244+
$existingDiscussion = $repoInfo.discussions.nodes | Where-Object { $_.title -eq $DISCUSSION_TITLE } | Select-Object -First 1
245+
if ($existingDiscussion) {
246+
$updateDiscussion = @'
247+
mutation($discussionId: ID!, $body: String!) {
248+
updateDiscussion(input: {discussionId: $discussionId, body: $body}) {
249+
discussion { url }
250+
}
251+
}
252+
'@
253+
& gh api graphql -f query="$updateDiscussion" -f discussionId="$($existingDiscussion.id)" -f body="$DISCUSSION_BODY" | Out-Null
254+
$discussionUrl = $existingDiscussion.url
255+
} else {
256+
$createDiscussion = @'
257+
mutation($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
258+
createDiscussion(input: {repositoryId: $repositoryId, categoryId: $categoryId, title: $title, body: $body}) {
259+
discussion { url }
260+
}
261+
}
262+
'@
263+
$createdDiscussion = & gh api graphql -f query="$createDiscussion" -f repositoryId="$($repoInfo.id)" -f categoryId="$($category.id)" -f title="$DISCUSSION_TITLE" -f body="$DISCUSSION_BODY" | ConvertFrom-Json
264+
$discussionUrl = $createdDiscussion.data.createDiscussion.discussion.url
265+
}
266+
} catch {
267+
Write-Warning "Discussion creation/update failed; falling back to issue. $($_.Exception.Message)"
268+
$discussionIssue = Ensure-Issue -Title $DISCUSSION_TITLE -Body $DISCUSSION_BODY -Labels @("docs")
269+
$discussionUrl = $discussionIssue.url
270+
}
271+
272+
$roadmapIssue = Ensure-Issue -Title $ROADMAP_ISSUE_TITLE -Body $ROADMAP_BODY_TEMPLATE -Labels @("chore", "release", "P0")
273+
$roadmapUrl = $roadmapIssue.url
274+
$roadmapNumber = $roadmapIssue.number
275+
276+
$taskDefinitions = @(
277+
@{ Title = "P0: Fix Machine-scope security for ProgramData bin (prevent Users write)"; Milestone = $milestoneP0; Labels = @("P0", "security", "installer", "bug") },
278+
@{ Title = "P0: Release republish same tag (clobber existing assets safely)"; Milestone = $milestoneP0; Labels = @("P0", "release", "enhancement") },
279+
@{ Title = "P0: Add workflow_dispatch to release workflow + manual rerun support"; Milestone = $milestoneP0; Labels = @("P0", "release", "enhancement") },
280+
@{ Title = "P0: Update/Upgrade guardrails for system scope (admin/elevation required)"; Milestone = $milestoneP0; Labels = @("P0", "upgrade", "security", "bug") },
281+
@{ Title = "P0: Repo hygiene (.gitignore) to prevent committing bin/dist/artifacts"; Milestone = $milestoneP0; Labels = @("P0", "chore") },
282+
@{ Title = "P1: Support bundle command (zip logs+config+doctor+paths+status)"; Milestone = $milestoneP1; Labels = @("P1", "enhancement", "docs") },
283+
@{ Title = "P1: Upgrade channels (stable/beta) + pinned version + target version install"; Milestone = $milestoneP1; Labels = @("P1", "upgrade", "enhancement") },
284+
@{ Title = "P1: GitHub API hardening (token support, retries, rate-limit messaging)"; Milestone = $milestoneP1; Labels = @("P1", "upgrade", "chore") },
285+
@{ Title = "P1: Proxy checksum verification robustness (deterministic source)"; Milestone = $milestoneP1; Labels = @("P1", "release", "bug") },
286+
@{ Title = "P1: Improve portable upgrade swap (temp + atomic replace + rollback)"; Milestone = $milestoneP1; Labels = @("P1", "upgrade", "enhancement") },
287+
@{ Title = "P1: NPM publish pipeline + package contents control"; Milestone = $milestoneP1; Labels = @("P1", "npm", "release", "enhancement") },
288+
@{ Title = "P2: Code signing for exe + installer (CI integration)"; Milestone = $milestoneP2; Labels = @("P2", "security", "release", "enhancement") },
289+
@{ Title = "P2: winget/choco/scoop distribution"; Milestone = $milestoneP2; Labels = @("P2", "release", "enhancement") },
290+
@{ Title = "P2: Enterprise policy.json (updates/auth constraints, rollout control)"; Milestone = $milestoneP2; Labels = @("P2", "security", "enhancement") },
291+
@{ Title = "P2: Service-aware upgrade coordination (stop/start service safely)"; Milestone = $milestoneP2; Labels = @("P2", "service", "enhancement") }
292+
)
293+
294+
$issueUrlMap = @{}
295+
foreach ($task in $taskDefinitions) {
296+
$issue = Ensure-Issue -Title $task.Title -Body "" -Labels $task.Labels -Milestone $task.Milestone
297+
$issueUrlMap[$task.Title] = $issue.url
298+
}
299+
300+
$milestoneUrls = $milestoneTitles | ForEach-Object { "https://github.com/$owner/$repo/milestone/$($milestoneMap[$_])" }
301+
302+
$p0Titles = $taskDefinitions | Where-Object { $_.Milestone -eq $milestoneP0 } | ForEach-Object { $_.Title }
303+
$p1Titles = $taskDefinitions | Where-Object { $_.Milestone -eq $milestoneP1 } | ForEach-Object { $_.Title }
304+
$p2Titles = $taskDefinitions | Where-Object { $_.Milestone -eq $milestoneP2 } | ForEach-Object { $_.Title }
305+
306+
$roadmapLines = @()
307+
$roadmapLines += "---"
308+
$roadmapLines += "# Roadmap: Production Finalization (P0/P1/P2) $enDash CloudSQLCTL"
309+
$roadmapLines += ""
310+
$roadmapLines += "## Links"
311+
$roadmapLines += "- Discussion: $discussionUrl"
312+
$roadmapLines += "- Milestones: $($milestoneUrls -join ' ')"
313+
$roadmapLines += "- Repo: https://github.com/$owner/$repo"
314+
$roadmapLines += ""
315+
$roadmapLines += ("This issue tracks all work required to reach {0}production final{1}." -f $leftQuote, $rightQuote)
316+
$roadmapLines += "Checklist items below link to the corresponding issues."
317+
$roadmapLines += ""
318+
$roadmapLines += "## $milestoneP0"
319+
foreach ($title in $p0Titles) {
320+
$roadmapLines += "- [ ] [$title]($($issueUrlMap[$title]))"
321+
}
322+
$roadmapLines += ""
323+
$roadmapLines += "## $milestoneP1"
324+
foreach ($title in $p1Titles) {
325+
$roadmapLines += "- [ ] [$title]($($issueUrlMap[$title]))"
326+
}
327+
$roadmapLines += ""
328+
$roadmapLines += "## $milestoneP2"
329+
foreach ($title in $p2Titles) {
330+
$roadmapLines += "- [ ] [$title]($($issueUrlMap[$title]))"
331+
}
332+
$roadmapLines += ""
333+
$roadmapLines += "## Notes"
334+
$roadmapLines += "- Use labels: Priority:P0/P1/P2, Area:*, Type:*."
335+
$roadmapLines += "- Each milestone should have a clear scope and DoD."
336+
$roadmapLines += "---"
337+
338+
$roadmapBody = $roadmapLines -join "`n"
339+
& gh issue edit $roadmapNumber --body $roadmapBody | Out-Null
340+
341+
Write-Host ""
342+
Write-Host "Discussion/Issue URL: $discussionUrl"
343+
Write-Host "Roadmap issue URL: $roadmapUrl"
344+
Write-Host "Milestones:"
345+
foreach ($title in $milestoneTitles) {
346+
Write-Host " $title (#$($milestoneMap[$title]))"
347+
}
348+
Write-Host "Issues:"
349+
foreach ($task in $taskDefinitions) {
350+
Write-Host " $($task.Title): $($issueUrlMap[$task.Title])"
351+
}

0 commit comments

Comments
 (0)