Documentation
¶
Overview ¶
Package sync fills the mirror from the configured sources (Jira, and Confluence when enabled). It only ever writes to the mirror, never to Jira or Confluence, and the rules it implements are in specs/000-product/contracts/sync.md.
Index ¶
- Constants
- Variables
- func FeedIdentity(cfg *config.Config) store.FeedIdentity
- func IsRejectedCredential(err error) bool
- func SyncIssue(ctx context.Context, cfg *config.Config, db *store.DB, key string, ...) error
- func SyncPage(ctx context.Context, cfg *config.Config, db *store.DB, id string) error
- func Watch(ctx context.Context, cfg *config.Config, db *store.DB, opts Options) error
- type Notifier
- type OSNotifier
- type Options
- type Result
Constants ¶
const ( PhaseIssues = "issues" PhaseDocuments = "documents" PhaseIdle = "" )
Phase source names for Options.Phase. Callers report what the mirror is fetching; Progress carries how much. Keep these string values stable — the web client keys UI copy on them.
const ConfluenceSourceID = "confluence"
ConfluenceSourceID is the slug the wiki connector owns in sources / sync_state.
const SourceID = "jira"
SourceID is the slug the Jira connector owns in `sources` and `sync_state`.
Variables ¶
var ErrNotFound = errors.New("sync: issue not found upstream")
ErrNotFound means the upstream answered but no issue/page with that key came back: it is outside the credential's permissions, or it was just deleted.
Functions ¶
func FeedIdentity ¶
func FeedIdentity(cfg *config.Config) store.FeedIdentity
FeedIdentity builds the personal-feed identity the same way the HTTP server does.
func IsRejectedCredential ¶ added in v0.14.2
IsRejectedCredential reports whether err is a dead credential from any source. Transport errors (500, timeout, DNS) must stay false.
Owner of the detection rule: atlhttp.ErrAuth (what Do returns on 401/403) or any error implementing atlhttp.RejectedCredential. A third source that uses atlhttp.Do is covered without a branch here. A source that is not built on atlhttp still implements the method. Exported for the server's sync progress document — every surface reads this one rule as a code instead of matching error prose.
func SyncIssue ¶
func SyncIssue(ctx context.Context, cfg *config.Config, db *store.DB, key string, opts Options) error
SyncIssue re-reads one issue and writes it to the mirror. Every write-through endpoint ends here, which is what makes a row that came back from a write identical to one a scheduled sync produced — same field mapping, same derived fields, no second code path to keep in step.
func SyncPage ¶
SyncPage re-reads one Confluence page and writes it to the mirror. Used when the desktop app closes an in-app browser tab after editing a wiki page: the rewrite moves synced_at and bumps sync_state.version so the client's next pages list / detail fetch sees the edit.
Unlike a full Confluence pass (commitBatch in confluence.go), this must not call RecordSync. A single page's lastModified must not become the source watermark — that would jump incremental sync into the future and skip every page modified in between. UpsertPages alone is enough: it rewrites the row and bumps version without advancing the watermark.
func Watch ¶
Watch runs incremental sync on an interval and reconcile on a longer one. A transport failure is logged and retried on the next tick; a rejected credential (IsRejectedCredential — atlhttp.ErrAuth or any error implementing atlhttp.RejectedCredential) records last_error, logs once, and stops retrying that source, because every further request would only burn rate budget. Jira is fatal (the loop ends). Confluence is not: Jira mirroring keeps going when only the wiki side is rejected. `gadak doctor` (sync.<id>.last_error) and sync_health read that last_error. `gadak status --json` last_error is the Jira row only. A later one-shot Run / RunConfluence with a new token still clears it. After each successful Jira cycle, new personal-feed events may produce one OS desktop notification (see notifyAfterSync); notification failures never stop the loop.
When opts.Reload is set, each cycle re-reads config so a settings edit (projects, Confluence, intervals) takes effect without restarting the process.
Types ¶
type Notifier ¶
Notifier delivers one OS desktop notification. Implementations must never panic; callers treat every error as non-fatal and continue the sync loop.
type OSNotifier ¶
type OSNotifier struct{}
OSNotifier uses the platform's desktop-notification command. darwin: osascript display notification; linux: notify-send; windows: no-op.
func (OSNotifier) Notify ¶
func (OSNotifier) Notify(title, body string) error
Notify implements Notifier.
type Options ¶
type Options struct {
Full bool
Reconcile bool
// Log, when set, receives one line per committed page and per pass.
Log func(string)
// Progress, when set, is called once per committed page with the running
// totals. It exists so a caller can report progress without parsing Log.
Progress func(fetched, changed int)
// Phase, when set, is called with the connector a pass is about to run
// ("issues", "documents") and with "" when the cycle has nothing in flight.
// It exists so a caller can report *what* is being fetched; Progress carries
// how much. Watch calls it around each source; one-shot callers set it too.
Phase func(source string)
// Client is for tests and for a server that wants to share one; nil builds
// one from cfg.
Client *jira.Client
// ConfluenceClient is for tests; nil builds one from cfg when Confluence is configured.
ConfluenceClient *confluence.Client
// Notifier delivers OS desktop alerts for new personal-feed events after
// each successful Watch cycle. Nil uses OSNotifier. Never aborts the loop.
Notifier Notifier
// Reload re-reads the config at the top of each watch cycle. Nil keeps the
// config Watch was called with. A reload error is logged and the previous
// config stays in use: a momentarily unreadable file must not stop the mirror.
Reload func() (*config.Config, error)
}
Options tunes one Run or Watch cycle. A nil Client or ConfluenceClient is built from cfg; a nil Notifier uses OSNotifier and never aborts the loop; a Reload error keeps the previous config so a momentarily unreadable file cannot stop the mirror.
type Result ¶
type Result struct {
Full bool
Fetched int
Changed int
Deleted int
Watermark string
// PageBodies and PageSkips are the Confluence pass's body-read tally: how
// many page bodies this pass went to the source for (a hit that turned out
// to be deleted still counts — the request was spent), and how many search
// hits pageFetchGate answered from the mirror instead. They are the answer
// to "how many bodies did that tick fetch?" — a quiet tick over an
// unchanged corpus must report PageBodies 0. Jira leaves both at 0.
PageBodies int
PageSkips int
}
Result is the tally of one source pass (Run or RunConfluence). Fetched/Changed/Deleted count what this pass touched. Full is true when the pass had no watermark to increment from, or the caller asked for one. Watermark is the newest upstream timestamp recorded on success.
func Run ¶
Run does one sync pass: full or incremental, plus a reconcile pass when asked for or after a full sync.
A failure leaves the pages already committed in place and does not advance the watermark, so the next run re-reads from the last known-good point (contracts/sync.md invariants 2 and 3).
An empty cfg.Projects means no project filter: the account's full visible issue set is the scope (one Search, not one per project).
func RunConfluence ¶
func RunConfluence(ctx context.Context, cfg *config.Config, db *store.DB, opts Options) (Result, error)
RunConfluence does one Confluence mirror pass: full or incremental. Attachments and changelog are out of scope for R1. Every successful pass prunes pages whose space is outside the current config/listing scope. Incremental floors are per-space (spaces.watermark); a failed space does not move its own watermark or any other space's. A floor selects candidates, it does not decide fetches: pageFetchGate does, and an incremental tick over an unchanged space reads zero page bodies.
A failure leaves already-committed batches in place.