You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
sync -project owner/N (or openspec/specsync.yml) resolves a BoardTarget, threads it through Options.Project, and Sync reports ItemResult.BoardConfigured from it — but never calls ProjectOntoBoard.
Only pull does (pull.go:134). The result: sync -project runs clean,
reports success, BoardConfigured: true, and silently does nothing to the
board. printBoardPlan (cmd/specsync/main.go:887) prints nothing useful
either, since it's handed a permanently-zero-value BoardPlan — so there's
no error, no warning, just quiet non-function exactly matching the
documented, expected behavior in shape (BoardConfigured says true) while
doing none of it.
This is a regression, not a gap.git log -S ProjectOntoBoard -- sync.go
shows Sync used to call it, inline in a per-provider syncOne helper, with
a hand-rolled three-way-merge dance (query current board state, compare
against a saved binding, decide push-local vs. respect-human-move vs.
report-conflict) before deciding whether to call ProjectOntoBoard at all.
Commit 4177dc7 ("multi-provider sync (fan-out)") restructured Sync into
its current per-provider loop and dropped that whole block — Board: and
the ProjectOntoBoard call vanished from the diff; BoardConfigured: opts.Project.Configured() and the Board BoardPlan field on ItemResult
were left behind, reporting stale/empty data ever since. No test caught it: board_test.go only calls ProjectOntoBoard directly, never through Sync; TestBoardTargetCarriesStatusMapping (added for an earlier,
narrower version of this same class of bug — BoardTarget.StatusMapping
reaching the CLI) only checks target construction, not that projection runs.
The fix is smaller than the regression, though.ProjectOntoBoard
itself (board.go:98) has been rewritten since the fan-out refactor into a
fully self-contained operation: it resolves the board schema, detects human
moves in both directions (HumanMovedToDone/HumanMovedToActive),
protects against clobbering a human-set Status via LastWrittenOptionID-based reasoning, and saves its own board binding
(changeDir param) — internally, on every call. None of the external
three-way-merge machinery the old syncOne had to hand-roll is needed
anymore; pull.go already proves this by calling ProjectOntoBoard with a
single line. Restoring the call to Sync is that same single line, not a
rebuild of the old logic.
Proposed Changes
In Sync's per-provider loop (sync.go), after Push succeeds, call bp.ProjectOntoBoard(ctx, opts.Project, ref, item, opts.DryRun, c.Dir)
when opts.Project.Configured() and the provider implements BoardProjector — mirroring pull.go's existing call exactly. It already
handles dry-run internally (a zero-API preview), so no extra dry-run
branching is needed here.
Capture the first successful plan into ItemResult.Board (first-provider-
wins, matching the existing firstRef/firstCreated pattern for
URL/created reporting) so printBoardPlan finally has real data.
A ProjectOntoBoard error is non-fatal to the change's sync (the issue
itself already pushed successfully) but printed as a warning to stderr —
silence is exactly the failure mode this proposal exists to remove, so a
new failure path here must not repeat it.
Non-Goals
Not touching DepSync's existing error handling
(sync.go, dependency-edge reconcile) even though it has the same
silent-swallow shape (if _, err := DepSync(...); err != nil { /* Log and continue */ } — no logging actually happens). Same bug family, separate
fix, separate PR.
Not reintroducing the old inline three-way-merge dance from before
the fan-out refactor — superseded by ProjectOntoBoard's own internal
handling, per the Why section.
Not per-provider board reporting in ProviderResult for multi-board
fan-out (multiple providers simultaneously implementing BoardProjector)
— first-provider-wins is enough today since GitHub is the only BoardProjector implementation that exists.
Open Questions
Should a ProjectOntoBoard failure also skip DepSync and any later
post-push step for that provider in the same run, or are they
independent enough to both attempt regardless? Leaning independent —
they touch unrelated state (board vs. issue cross-links).
Release Notes
Fixed: sync -project (and openspec/specsync.yml's board target) now
actually projects issues onto the configured GitHub Projects board — it
previously reported BoardConfigured: true and silently did nothing. pull was unaffected; this restores parity for sync.
Tasks
1. In Sync's per-provider loop (sync.go), after ref, perr := prov.Push(...) succeeds, add: when opts.Project.Configured() and prov implements BoardProjector, call bp.ProjectOntoBoard(ctx, opts.Project, ref, item, opts.DryRun, c.Dir).
Track the first successful plan into a boardPlan BoardPlan local
(mirroring the existing firstRef/firstCreated pattern), and set ItemResult.Board: boardPlan in the res.Items = append(...) call
that already sets BoardConfigured.
Validation: a fixture Sync call with a configured BoardTarget
against a BoardProjector-implementing fake provider results in a ProjectOntoBoard call being made (mirrors pull_test.go's existing
coverage of the same call via Pull) and ItemResult.Board non-zero.
2. A ProjectOntoBoard error is non-fatal: print fmt.Fprintf(os.Stderr, "specsync: warning: board projection for %s failed: %v\n", c.Slug, err) and continue the rest of that provider's
post-push steps (dependency sync, etc.) rather than continue-ing
past them. Add the os import to sync.go.
Validation: a fixture where the fake BoardProjector returns an error
still completes the sync (issue created/updated, no early exit) and
the warning is observable (captured stderr, or the function signature
exposes it for the test — match whatever pattern existing warnings in
this codebase use, e.g. pull.go's intake-transition warning).
3. Regression test proving the specific regression this proposal
describes: Sync with Options.Project configured against a BoardProjector fake provider must result in exactly one ProjectOntoBoard call per change per provider — not zero (the bug),
not more than one.
4. Dry-run coverage: Sync with DryRun: true and a configured board
target still calls ProjectOntoBoard (it handles dry-run internally,
zero API calls) and the resulting ItemResult.Board is non-zero, so -dry-run output actually previews the board plan instead of the CLI
printing nothing under "board:".
5. Manual/live verification: ran specsync -dry-run -change wire-sync-board-projection -project androidand/2 against a real
GitHub Projects board with both the unpatched and patched binary.
Unpatched: only the static "would ensure the issue is on the board"
line printed (StatusName/AssigneeLogin always empty). Patched: all
three preview lines printed (add/status/assignee), confirming the
exact silent-no-op this proposal describes and its fix. Did not
perform a real (non-dry-run) write against that board — it is an
unrelated project (BrickNow Prioritization), not a low-stakes test
board for this repo, so a live mutation was skipped pending a real
target.
6. No documentation change needed for accuracy — README.md:592-599
("sync + project onto board 6") and site/features.json's "Projects
board sync" entry already correctly describe the intended behavior;
this proposal makes reality match what's already documented, not the
other way round.
Proposal
Why
sync -project owner/N(oropenspec/specsync.yml) resolves aBoardTarget, threads it throughOptions.Project, andSyncreportsItemResult.BoardConfiguredfrom it — but never callsProjectOntoBoard.Only
pulldoes (pull.go:134). The result:sync -projectruns clean,reports success,
BoardConfigured: true, and silently does nothing to theboard.
printBoardPlan(cmd/specsync/main.go:887) prints nothing usefuleither, since it's handed a permanently-zero-value
BoardPlan— so there'sno error, no warning, just quiet non-function exactly matching the
documented, expected behavior in shape (
BoardConfiguredsays true) whiledoing none of it.
This is a regression, not a gap.
git log -S ProjectOntoBoard -- sync.goshows
Syncused to call it, inline in a per-providersyncOnehelper, witha hand-rolled three-way-merge dance (query current board state, compare
against a saved binding, decide push-local vs. respect-human-move vs.
report-conflict) before deciding whether to call
ProjectOntoBoardat all.Commit
4177dc7("multi-provider sync (fan-out)") restructuredSyncintoits current per-provider loop and dropped that whole block —
Board:andthe
ProjectOntoBoardcall vanished from the diff;BoardConfigured: opts.Project.Configured()and theBoard BoardPlanfield onItemResultwere left behind, reporting stale/empty data ever since. No test caught it:
board_test.goonly callsProjectOntoBoarddirectly, never throughSync;TestBoardTargetCarriesStatusMapping(added for an earlier,narrower version of this same class of bug —
BoardTarget.StatusMappingreaching the CLI) only checks target construction, not that projection runs.
The fix is smaller than the regression, though.
ProjectOntoBoarditself (
board.go:98) has been rewritten since the fan-out refactor into afully self-contained operation: it resolves the board schema, detects human
moves in both directions (
HumanMovedToDone/HumanMovedToActive),protects against clobbering a human-set Status via
LastWrittenOptionID-based reasoning, and saves its own board binding(
changeDirparam) — internally, on every call. None of the externalthree-way-merge machinery the old
syncOnehad to hand-roll is neededanymore;
pull.goalready proves this by callingProjectOntoBoardwith asingle line. Restoring the call to
Syncis that same single line, not arebuild of the old logic.
Proposed Changes
Sync's per-provider loop (sync.go), afterPushsucceeds, callbp.ProjectOntoBoard(ctx, opts.Project, ref, item, opts.DryRun, c.Dir)when
opts.Project.Configured()and the provider implementsBoardProjector— mirroringpull.go's existing call exactly. It alreadyhandles dry-run internally (a zero-API preview), so no extra dry-run
branching is needed here.
ItemResult.Board(first-provider-wins, matching the existing
firstRef/firstCreatedpattern forURL/created reporting) so
printBoardPlanfinally has real data.ProjectOntoBoarderror is non-fatal to the change's sync (the issueitself already pushed successfully) but printed as a warning to stderr —
silence is exactly the failure mode this proposal exists to remove, so a
new failure path here must not repeat it.
Non-Goals
DepSync's existing error handling(
sync.go, dependency-edge reconcile) even though it has the samesilent-swallow shape (
if _, err := DepSync(...); err != nil { /* Log and continue */ }— no logging actually happens). Same bug family, separatefix, separate PR.
the fan-out refactor — superseded by
ProjectOntoBoard's own internalhandling, per the Why section.
ProviderResultfor multi-boardfan-out (multiple providers simultaneously implementing
BoardProjector)— first-provider-wins is enough today since GitHub is the only
BoardProjectorimplementation that exists.Open Questions
ProjectOntoBoardfailure also skipDepSyncand any laterpost-push step for that provider in the same run, or are they
independent enough to both attempt regardless? Leaning independent —
they touch unrelated state (board vs. issue cross-links).
Release Notes
Fixed:
sync -project(andopenspec/specsync.yml's board target) nowactually projects issues onto the configured GitHub Projects board — it
previously reported
BoardConfigured: trueand silently did nothing.pullwas unaffected; this restores parity forsync.Tasks
1. In
Sync's per-provider loop (sync.go), afterref, perr := prov.Push(...)succeeds, add: whenopts.Project.Configured()andprovimplementsBoardProjector, callbp.ProjectOntoBoard(ctx, opts.Project, ref, item, opts.DryRun, c.Dir).Track the first successful plan into a
boardPlan BoardPlanlocal(mirroring the existing
firstRef/firstCreatedpattern), and setItemResult.Board: boardPlanin theres.Items = append(...)callthat already sets
BoardConfigured.Validation: a fixture
Synccall with a configuredBoardTargetagainst a
BoardProjector-implementing fake provider results in aProjectOntoBoardcall being made (mirrorspull_test.go's existingcoverage of the same call via
Pull) andItemResult.Boardnon-zero.2. A
ProjectOntoBoarderror is non-fatal: printfmt.Fprintf(os.Stderr, "specsync: warning: board projection for %s failed: %v\n", c.Slug, err)and continue the rest of that provider'spost-push steps (dependency sync, etc.) rather than
continue-ingpast them. Add the
osimport tosync.go.Validation: a fixture where the fake
BoardProjectorreturns an errorstill completes the sync (issue created/updated, no early exit) and
the warning is observable (captured stderr, or the function signature
exposes it for the test — match whatever pattern existing warnings in
this codebase use, e.g.
pull.go's intake-transition warning).3. Regression test proving the specific regression this proposal
describes:
SyncwithOptions.Projectconfigured against aBoardProjectorfake provider must result in exactly oneProjectOntoBoardcall per change per provider — not zero (the bug),not more than one.
4. Dry-run coverage:
SyncwithDryRun: trueand a configured boardtarget still calls
ProjectOntoBoard(it handles dry-run internally,zero API calls) and the resulting
ItemResult.Boardis non-zero, so-dry-runoutput actually previews the board plan instead of the CLIprinting nothing under "board:".
5. Manual/live verification: ran
specsync -dry-run -change wire-sync-board-projection -project androidand/2against a realGitHub Projects board with both the unpatched and patched binary.
Unpatched: only the static "would ensure the issue is on the board"
line printed (StatusName/AssigneeLogin always empty). Patched: all
three preview lines printed (add/status/assignee), confirming the
exact silent-no-op this proposal describes and its fix. Did not
perform a real (non-dry-run) write against that board — it is an
unrelated project (BrickNow Prioritization), not a low-stakes test
board for this repo, so a live mutation was skipped pending a real
target.
6. No documentation change needed for accuracy —
README.md:592-599("sync + project onto board 6") and
site/features.json's "Projectsboard sync" entry already correctly describe the intended behavior;
this proposal makes reality match what's already documented, not the
other way round.
Plan changes
6 done