# Session Log: Comparative Review of Two JIRA→P4 Jobs Implementations **Date:** 2026-08-29 **Author:** Claude (Claude Code session) **Requested by:** Tom Tyler --- ## Task Two independent AI sessions had each produced a full implementation of a one-way JIRA → P4 Jobs sync tool, developed against similar but independently derived requirements: 1. **Bash** — `/Users/ttyler/pub/j2j` (`bin/sync_jira_to_p4jobs.sh`), versioned in the Public Depot server at `//j2j/dev/...`. Target: syncing `perforce.atlassian.net` SDP issues into Public Depot P4 jobs (`workshop.perforce.com`) for public visibility. 2. **Python** — `/Users/ttyler/ppn/user/JIRA2P4Jobs` (`bin/JIRAtoP4Jobs.py` + `bin/JIRAtoP4Jobs.sh` wrapper), versioned in the PPN server at `//user/ttyler/JIRA2P4Jobs/...`. Neither had been executed, even in dry-run/no-op mode. Goal: evaluate both against their (explicit or inferred) requirements, decide which to lead with, and identify anything from the other worth folding in — including error handling ideas. --- ## Requirements (stated + inferred) Both tools converge on the same core contract, which is a good sign it's the right shape for the problem: - **One-way, read-only mirror.** JIRA is always the source of truth. The script must never write to JIRA, and P4 jobs are never authoritative. - **Incremental.** Only poll/process issues updated since some prior point; don't reprocess the whole project every run. - **Idempotent.** Safe to run repeatedly / on a schedule without side effects from re-processing the same issue twice. - **Fail-safe.** Outages (JIRA down, P4 down, auth failure) must not corrupt state or silently skip data — safe to retry. - **Field mapping.** JIRA status/type/summary/description/reporter/dates need translating into the Public Depot jobspec's fields (`Status`, `Type`, `Project`, `Severity`, `ReportedBy`, dates, `Description`). - **Dry-run capable**, since this touches a public-facing production job database and neither tool has ever been run live. --- ## Side-by-side | Dimension | Bash (`pub/j2j`) | Python (`ppn/.../JIRA2P4Jobs`) | |---|---|---| | Last touched | 2026-08-29 (active) | 2026-06-06 (stale ~3 months) | | JIRA endpoint | `/rest/api/3/search/jql`, `nextPageToken` (current Atlassian Cloud API) | `/rest/api/2/search` (POST, `startAt`-style) — Atlassian has been retiring this generation of the search endpoint; **real risk this doesn't work against Cloud today** | | Pagination | Yes, loops on `nextPageToken` | **None** — fixed `maxResults=100`; issues beyond 100 in the window are silently dropped | | Incremental strategy | Persistent watermark file, advanced only after a fully clean run; JQL `updated >= watermark` | Fixed rolling window (`--hours`, default 24h) recomputed fresh every run — a gap of >24h between runs silently drops issues, no persisted state | | Concurrency safety | `flock` advisory lock | None | | Retry/backoff | Yes — 429/5xx retried with backoff, 4xx non-retryable, configurable attempts/delay | None — any HTTP hiccup is a hard `sys.exit` | | Auth (JIRA) | Basic auth, token from a file (`JiraTokenFile`) | Basic auth, token from env var (set by wrapper script from `secure/token.value`) | | Auth (P4) | Dedicated service account + stored ticket file, passed via `p4 -P ` | Ambient — inherits whatever `p4 login` state / env vars the calling shell already has; script never touches secrets itself | | Config | External `.cfg` file (`--init` scaffolds it), per-project mapping table | None — hardcoded JIRA URL, `argparse` flags for project/hours/execute | | Status mapping | 10 P4 states, matched on both specific status name and coarse category | 3 states only (`To Do`/`In Progress`/`Done` → `open`/`suspended`/`closed`) — and `In Progress → suspended` looks like a mapping bug (suspended ≠ in-progress) | | Type mapping | Yes → `Bug`/`Doc`/`Feature`/`Problem` | **None** | | Project field mapping | Yes — explicit `JiraProjectMap[]` config, since the jobspec `Project` field is an enum unrelated to the JIRA key | **None** — `Project` is never set on the write, so new jobs get whatever jobspec default applies, not a value derived from JIRA | | ReportedBy / dates | Set from JIRA `reporter`/`created`/`updated` | **None** — never set; new jobs get p4 defaults (wrong attribution/dates) | | Description format | ADF (Atlassian Document Format) walked and flattened to plain text | Treats `description` as a raw value; if JIRA Cloud returns ADF (a dict), an f-string will interpolate a Python dict repr into the job — **likely broken output** for any JIRA Cloud project using rich-text descriptions | | Fields preserved on update | Explicitly leaves `OwnedBy`, `DevNotes`, `Component`, `Release` untouched | Only touches `Job`/`Description`/`Status`; incidentally preserves everything else, but that's a side effect of doing less, not a deliberate design | | Change detection | Relies on JIRA-side incremental filter + idempotent overwrite | Explicitly fetches the existing job first and diffs Summary/Description/Status before writing — **a genuinely nice idea** | | P4 write mechanism | Text form via heredoc + `p4 job -i -f` | `p4 -G` (Python marshaled dicts) via `p4 job -i` — structurally safer, no form-syntax edge cases to worry about | | Per-item failure handling | Logs, counts, continues to next issue; only refuses to advance the watermark | Any single job save failure calls `sys.exit` — **aborts the entire run mid-batch** | | Dry-run default | Live by default; `-n` opts into preview | **Dry-run by default**; `--execute` opts into live writes | | Logging | Timestamped log files + stable symlink, verbosity levels 1–5, silent mode for cron | `print()` only | | Docs/standards | Full `-man` page, README, follows SDP Bash Coding Standard, ShellCheck findings addressed | One paragraph docstring, one small packages note | | Dependencies | `bash`, `curl`, `flock`, `python3` (stdlib only, no `jq`) | `python3`, `requests`, (originally `p4python`, later removed) | --- ## Verdict: lead with the Bash implementation Tom's instinct to favor the Bash script is correct, and not just on stylistic grounds. Beyond fitting the complexity level: - It's operationally consistent with SDP, which is otherwise all Bash — no venv/interpreter-version/package management burden on the Public Depot host. - It's materially more correct, not just more complete: the Python script has real functional gaps (no `Project`/`Type`/`ReportedBy`/date mapping, no ADF parsing, no pagination, a probably-deprecated JIRA search endpoint) that would need fixing before it could run correctly at all — this isn't a case of "less code, same result," it's a case of "less code, wrong result." Notably, the Python script's staleness (last touched three months ago, never revisited since) also lines up with this — it reads as an earlier, abandoned pass rather than a parallel finished candidate. - The Bash script's own author-recorded gaps (`ModifiedBy` accuracy, `Severity` hardcoded, `Component`/`OwnedBy` unmapped) are pre-existing, disclosed TODOs on an otherwise coherent design — not surprises found in review. **Recommendation: Bash is the lead implementation.** Retire/archive the Python one rather than continuing to invest in both. --- ## Ideas worth porting from Python into the Bash lead 1. **Flip the dry-run default.** Python defaults to dry-run and requires `--execute` to write; Bash defaults to live and requires `-n` to preview. Given this has *never been run*, safe-by-default is the better posture. Suggest changing Bash so a real run requires an explicit opt-in (e.g. require `--execute` or `-y`, keep `-n`/`--noop` as an explicit synonym for clarity) rather than making "forgot a flag" the failure mode that writes to a public job database. 2. **Don't put the P4 ticket on the command line.** `p4_cmd()` currently does `p4 -P "$P4Passwd" ...` — that ticket value is visible to any local user via `ps` for the life of the process. Python's approach (ambient auth, secret never touches an argv) sidesteps this entirely. For Bash, the cheap fix is exporting `P4PASSWD="$P4Passwd"` for the `p4` subprocess instead of passing `-P` as an argument. Worth fixing regardless of which tool leads. 3. **Diff-before-write (minor/optional).** Python's fetch-existing-job-and- compare-before-saving is a nice pattern to reduce needless writes on a full backfill (`InitialLookbackSeconds=0`) where every issue in a project gets reprocessed regardless of whether it actually changed. Not urgent — the watermark already limits scope to genuinely-changed issues on normal incremental runs — but worth considering for the initial full-sync path. 4. **Upfront JIRA auth check (minor/optional).** Python's explicit `/rest/api/*/myself` preflight gives an immediate, clear "bad token" error before doing any real work. Bash gets equivalent behavior implicitly (first `jira_get` call fails non-retryably on 401/403), so this is a nice-to-have for a clearer error message, not a functional gap. **Do not port:** Python's JIRA fetch logic (deprecated-looking endpoint, no pagination → silent data loss above 100 issues/window), its 3-value status map (including the apparent `In Progress → suspended` bug), or its missing `Project`/`Type`/`ReportedBy`/date field mapping. These are regressions relative to the Bash script, not alternatives to weigh. --- ## Other findings worth flagging (independent of which tool leads) - **Personal API token in use.** `ppn/.../secure/token.value` is Tom's own Atlassian API token (per `token.name`: `ttyler-api-token-expires-2027-06-06`), not a dedicated service account. Fine for a first dry run; before any scheduled/unattended production use, create a dedicated read-only JIRA service account so sync attribution/audit trail doesn't depend on a personal credential, and rotation doesn't require touching prod config. - **`p4 job -f` requires `admin` access.** This is an inherent Perforce limitation (there's no finer-grained protection just for job date fields), not something either script does wrong — just worth remembering that the service account needs a broad grant, so its ticket file should be tightly permissioned (already is: `chmod 600` per the README). - **First live test path:** run the Bash script `-n -v 5` against a single project (`-p SDP`) first, pointed at the already-provisioned token in `ppn/.../secure/token.value` via `JiraTokenFile` in the config. Review NoOp output carefully before dropping `-n`. --- ## Next steps 1. Apply the two concrete fixes above to the Bash script (safe-by-default dry-run, stop passing the P4 ticket as a CLI argument). 2. Point `JiraTokenFile` at the existing PPN token for a first `-n -v 5` test run against a single project. 3. Decide whether to archive or delete the PPN Python implementation, or keep it around only as a reference for the diff-before-write idea.