Date: 2026-08-29 Author: Claude (Claude Code session) Requested by: Tom Tyler
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:
/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./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.
Both tools converge on the same core contract, which is a good sign it's the right shape for the problem:
Status, Type,
Project, Severity, ReportedBy, dates, Description).| 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 <ticket> |
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) |
Tom's instinct to favor the Bash script is correct, and not just on stylistic grounds. Beyond fitting the complexity level:
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.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.
--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.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.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./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.
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).-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.JiraTokenFile at the existing PPN token for a first -n -v 5 test
run against a single project.# 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 <ticket>` | 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.
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #1 | 33523 | C. Thomas Tyler | Renamed folder. | ||
| //j2j/dev/ai/SESSION_2026-08-29_implementation_comparison.md | |||||
| #1 | 33354 | C. Thomas Tyler | Safe-by-default dry-run (--execute required to write); pass P4 ticket via P4PASSWD env var instead of -P argv; add .p4ignore for local secure/ token dir; add comparative review session log. | ||