sync_jira_to_p4jobs.sh — one-way, read-only sync of JIRA issues into the
Perforce Public Depot job database.
This script is not part of the SDP package. Per SDP convention, software components that are deployed in an SDP environment but are not part of the SDP live under the site directory:
/p4/common/site/
The SDP environment file (p4_vars) exposes these shell variables for the
site tree:
| Variable | Default path | Purpose |
|---|---|---|
$P4CSITE |
/p4/common/site |
Site root |
$P4CSBIN |
/p4/common/site/bin |
Site scripts and executables |
$P4CBIN (/p4/common/bin) is reserved for scripts that are part of
the SDP package and should not be used for site-local additions.
All paths in this document and in the script's example config follow this
convention. Sensitive credential files are kept under /p4/common/site/etc/,
which is inside the site tree and outside the SDP-managed directories.
┌─────────────────────────┐
JIRA Cloud │ perforce.atlassian.net │
(source of truth) │ /rest/api/3/search/jql │
└────────────┬────────────┘
│ HTTPS (read-only, API token)
│ incremental (JQL: updated >= watermark)
▼
Public Depot host ┌─────────────────────────┐
│ sync_jira_to_p4jobs.sh │
│ /p4/common/site/bin/ │
│ • polls JIRA │
│ • maps fields │
│ • upserts P4 jobs │
│ • advances watermark │
└────────────┬────────────┘
│ p4 job -i -f (admin, local socket)
▼
┌─────────────────────────┐
│ p4d (Public Depot) │
│ workshop.perforce.com │
└─────────────────────────┘
JIRA is never written to. P4 jobs are never the source of truth; they are a mirror maintained by this script.
| Requirement | Notes |
|---|---|
bash 4.x+ |
Standard on Ubuntu 20+ / RHEL 8+ |
python3 |
Used for JSON parsing (no external libs needed) |
curl |
For JIRA API calls |
flock |
For advisory locking (part of util-linux) |
p4 CLI |
Must be on $PATH |
| P4 admin account | Needs admin access in p4 protect for p4 job -f |
| JIRA API token | Read-only is sufficient |
cp sync_jira_to_p4jobs.sh /p4/common/site/bin/
chmod 755 /p4/common/site/bin/sync_jira_to_p4jobs.sh
/p4/common/site/bin/sync_jira_to_p4jobs.sh --init
# writes /p4/common/site/bin/sync_jira_to_p4jobs.cfg
Edit the config file; all fields are documented inline.
JiraTokenFile:mkdir -p /p4/common/site/etc
echo -n 'paste-token-here' > /p4/common/site/etc/.jira_api_token
chmod 600 /p4/common/site/etc/.jira_api_token
chown perforce:perforce /p4/common/site/etc/.jira_api_token
# As a P4 super user:
p4 user -f svc-jira-sync # fill in Name/Email, save
p4 protect # grant admin to svc-jira-sync
# Log in and save the ticket:
p4 -u svc-jira-sync -p ssl:workshop.perforce.com:1666 login -p \
> /p4/common/site/etc/.p4_jira_sync_ticket
chmod 600 /p4/common/site/etc/.p4_jira_sync_ticket
chown perforce:perforce /p4/common/site/etc/.p4_jira_sync_ticket
Why
adminaccess? Thep4 job -fflag is required to set theReportedDateandModifiedDatefields, which are markedonceandalwaysrespectively in the jobspec and are otherwise read-only.
The Public Depot jobspec Project field is a select (enum). Each JIRA
project key must be mapped to its corresponding jobspec value. Edit the
config file and add one JiraProjectMap line per project:
JiraProjectMap[SDP]=perforce-software-sdp
To discover valid jobspec Project values:
p4 jobspec -o | grep -A1 'Project$'
p4 jobspec -o
The script writes these fields: Job, Status, Project, Severity,
Type, ReportedBy, ReportedDate, ModifiedBy, ModifiedDate,
Description. Fields not written (OwnedBy, DevNotes, Component,
Release) retain their existing values on update.
# Dry run (no changes):
/p4/common/site/bin/sync_jira_to_p4jobs.sh -n -v 5
# Real run for a single project, verbose:
/p4/common/site/bin/sync_jira_to_p4jobs.sh -p SDP -v 4
# Sync JIRA → P4 jobs every 30 minutes (as perforce user)
*/30 * * * * /p4/common/site/bin/sync_jira_to_p4jobs.sh -si
Or with a systemd timer (preferred on modern Ubuntu):
# /etc/systemd/system/jira-p4-sync.service
[Unit]
Description=Sync JIRA issues to P4 Public Depot jobs
[Service]
Type=oneshot
User=perforce
ExecStart=/p4/common/site/bin/sync_jira_to_p4jobs.sh -si
StandardOutput=append:/p4/1/logs/sync_jira_to_p4jobs.log
StandardError=append:/p4/1/logs/sync_jira_to_p4jobs.log
# /etc/systemd/system/jira-p4-sync.timer
[Unit]
Description=Run JIRA→P4 sync every 30 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=30min
[Install]
WantedBy=timers.target
systemctl enable --now jira-p4-sync.timer
systemctl status jira-p4-sync.timer
Note on log paths: The
$LOGSSDP environment variable (set byp4_vars) points to/p4/<instance>/logs, which is the correct location for runtime logs regardless of where the script itself lives. The script uses$LOGSautomatically when it is set in the environment; the systemd unit above uses an explicit path as a fallback for whenp4_varshas not been sourced.
| P4 field | Source | Notes |
|---|---|---|
Job |
JIRA issue key | e.g. SDP-1234 |
Status |
JIRA status name + category | Mapped to jobspec values (see below) |
Project |
JIRA project key | Via JiraProjectMap[] in config |
Severity |
Fixed: C |
JIRA has no direct equivalent |
Type |
JIRA issue type | Bug / Doc / Feature / Problem |
ReportedBy |
fields.reporter email or displayName |
|
ReportedDate |
fields.created |
YYYY/MM/DD |
ModifiedBy |
fields.reporter (best available) |
JIRA updater not in search API |
ModifiedDate |
fields.updated |
YYYY/MM/DD |
Description |
First line = JIRA summary; blank line; body | Standard P4 convention |
| JIRA status (specific name, pattern) | P4 Status |
|---|---|
matches *block* |
blocked |
matches *review* |
review |
matches *duplicate* |
duplicate |
matches *obsolete*, *wontfix* |
obsolete |
matches *punt* |
punted |
matches *done*, *resolved*, *complete* |
closed |
category = To Do / New |
open |
category = In Progress |
inprogress |
category = Done (fallback) |
closed |
| (anything else) | open |
The first line of the P4 Description field doubles as the issue title
(JIRA summary). This is a long-standing P4 Jobs convention on the Public
Depot: tools and views that show only one line of a job's description
show the summary. The full structure is:
<JIRA summary line>
<blank line>
<JIRA description body>
| Failure scenario | Behaviour |
|---|---|
| JIRA is down | curl times out / retries. Watermark NOT advanced. Next run retries from same point. |
| JIRA rate-limits (HTTP 429) | Back-off and retry within current run. |
| Single JIRA page fails | Project aborted; watermark NOT advanced. |
| P4 job upsert fails | Logged; script continues with next issue. Watermark NOT advanced if any failures. |
| P4 is down | p4 info preflight fails; script exits immediately. Watermark NOT advanced. |
| Script killed mid-run | flock lock released on exit. Watermark not advanced. Next run re-fetches idempotently. |
| Concurrent invocations | flock prevents overlap; second instance exits immediately. |
The JobIncrement.pl trigger fires on form-in events for jobs whose
Job: field value is new. Since this script always supplies an
explicit job name (the JIRA issue key, e.g. SDP-1234), the trigger
exits immediately without any action. Other projects using JobIncrement
with their own prefixes are completely unaffected.
| File | Purpose |
|---|---|
$StateDir/last_sync_time |
Watermark of last successful poll. Delete to force a full re-sync. |
$StateDir/last_sync.lock |
flock advisory lock. Safe to delete when no instance is running. |
$StateDir/jira_page_cache/*.json |
Raw JIRA JSON pages; retained 2 days for diagnostics. |
StateDir defaults to /p4/1/tmp/jira_sync (instance-1 tmp area); override
in the config file if needed.
rm /p4/1/tmp/jira_sync/last_sync_time
/p4/common/site/bin/sync_jira_to_p4jobs.sh -v 4
/rest/api/3/search/jql with nextPageToken pagination (the old
startAt-based /rest/api/3/search was removed from Atlassian Cloud in 2025).updated >= watermark,
keeping each poll small and incremental.JiraPollDelay (default 2 s) is inserted after every successful API call.p4-public-depot-sync).JiraTokenFile as described in step 3 of Installation.# JIRA → P4 Jobs Sync
`sync_jira_to_p4jobs.sh` — one-way, read-only sync of JIRA issues into the
Perforce Public Depot job database.
## SDP site directory convention
This script is **not** part of the SDP package. Per SDP convention, software
components that are deployed *in* an SDP environment but are not *part of* the
SDP live under the **site directory**:
```
/p4/common/site/
```
The SDP environment file (`p4_vars`) exposes these shell variables for the
site tree:
| Variable | Default path | Purpose |
|------------|---------------------------|----------------------------------|
| `$P4CSITE` | `/p4/common/site` | Site root |
| `$P4CSBIN` | `/p4/common/site/bin` | Site scripts and executables |
`$P4CBIN` (`/p4/common/bin`) is reserved for scripts that **are** part of
the SDP package and should not be used for site-local additions.
All paths in this document and in the script's example config follow this
convention. Sensitive credential files are kept under `/p4/common/site/etc/`,
which is inside the site tree and outside the SDP-managed directories.
## Architecture overview
```
┌─────────────────────────┐
JIRA Cloud │ perforce.atlassian.net │
(source of truth) │ /rest/api/3/search/jql │
└────────────┬────────────┘
│ HTTPS (read-only, API token)
│ incremental (JQL: updated >= watermark)
▼
Public Depot host ┌─────────────────────────┐
│ sync_jira_to_p4jobs.sh │
│ /p4/common/site/bin/ │
│ • polls JIRA │
│ • maps fields │
│ • upserts P4 jobs │
│ • advances watermark │
└────────────┬────────────┘
│ p4 job -i -f (admin, local socket)
▼
┌─────────────────────────┐
│ p4d (Public Depot) │
│ workshop.perforce.com │
└─────────────────────────┘
```
JIRA is **never written to**. P4 jobs are **never the source of truth**;
they are a mirror maintained by this script.
---
## Prerequisites
| Requirement | Notes |
|---|---|
| `bash` 4.x+ | Standard on Ubuntu 20+ / RHEL 8+ |
| `python3` | Used for JSON parsing (no external libs needed) |
| `curl` | For JIRA API calls |
| `flock` | For advisory locking (part of `util-linux`) |
| `p4` CLI | Must be on `$PATH` |
| P4 admin account | Needs `admin` access in `p4 protect` for `p4 job -f` |
| JIRA API token | Read-only is sufficient |
---
## Installation
### 1. Place the script
```bash
cp sync_jira_to_p4jobs.sh /p4/common/site/bin/
chmod 755 /p4/common/site/bin/sync_jira_to_p4jobs.sh
```
### 2. Generate an example config
```bash
/p4/common/site/bin/sync_jira_to_p4jobs.sh --init
# writes /p4/common/site/bin/sync_jira_to_p4jobs.cfg
```
Edit the config file; all fields are documented inline.
### 3. Create the JIRA API token
1. Log in to Atlassian as the service account.
2. Go to **Account settings → Security → API tokens → Create API token**.
3. Save the token to the path set in `JiraTokenFile`:
```bash
mkdir -p /p4/common/site/etc
echo -n 'paste-token-here' > /p4/common/site/etc/.jira_api_token
chmod 600 /p4/common/site/etc/.jira_api_token
chown perforce:perforce /p4/common/site/etc/.jira_api_token
```
### 4. Create the P4 service account and ticket
```bash
# As a P4 super user:
p4 user -f svc-jira-sync # fill in Name/Email, save
p4 protect # grant admin to svc-jira-sync
# Log in and save the ticket:
p4 -u svc-jira-sync -p ssl:workshop.perforce.com:1666 login -p \
> /p4/common/site/etc/.p4_jira_sync_ticket
chmod 600 /p4/common/site/etc/.p4_jira_sync_ticket
chown perforce:perforce /p4/common/site/etc/.p4_jira_sync_ticket
```
> **Why `admin` access?** The `p4 job -f` flag is required to set the
> `ReportedDate` and `ModifiedDate` fields, which are marked `once` and
> `always` respectively in the jobspec and are otherwise read-only.
### 5. Configure the JIRA project → P4 project mapping
The Public Depot jobspec `Project` field is a select (enum). Each JIRA
project key must be mapped to its corresponding jobspec value. Edit the
config file and add one `JiraProjectMap` line per project:
```ini
JiraProjectMap[SDP]=perforce-software-sdp
```
To discover valid jobspec Project values:
```bash
p4 jobspec -o | grep -A1 'Project$'
```
### 6. Verify the jobspec field names
```bash
p4 jobspec -o
```
The script writes these fields: `Job`, `Status`, `Project`, `Severity`,
`Type`, `ReportedBy`, `ReportedDate`, `ModifiedBy`, `ModifiedDate`,
`Description`. Fields not written (`OwnedBy`, `DevNotes`, `Component`,
`Release`) retain their existing values on update.
### 7. Test manually
```bash
# Dry run (no changes):
/p4/common/site/bin/sync_jira_to_p4jobs.sh -n -v 5
# Real run for a single project, verbose:
/p4/common/site/bin/sync_jira_to_p4jobs.sh -p SDP -v 4
```
### 8. Schedule with cron
```cron
# Sync JIRA → P4 jobs every 30 minutes (as perforce user)
*/30 * * * * /p4/common/site/bin/sync_jira_to_p4jobs.sh -si
```
Or with a systemd timer (preferred on modern Ubuntu):
```ini
# /etc/systemd/system/jira-p4-sync.service
[Unit]
Description=Sync JIRA issues to P4 Public Depot jobs
[Service]
Type=oneshot
User=perforce
ExecStart=/p4/common/site/bin/sync_jira_to_p4jobs.sh -si
StandardOutput=append:/p4/1/logs/sync_jira_to_p4jobs.log
StandardError=append:/p4/1/logs/sync_jira_to_p4jobs.log
```
```ini
# /etc/systemd/system/jira-p4-sync.timer
[Unit]
Description=Run JIRA→P4 sync every 30 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=30min
[Install]
WantedBy=timers.target
```
```bash
systemctl enable --now jira-p4-sync.timer
systemctl status jira-p4-sync.timer
```
> **Note on log paths:** The `$LOGS` SDP environment variable (set by
> `p4_vars`) points to `/p4/<instance>/logs`, which is the correct location
> for runtime logs regardless of where the script itself lives. The script
> uses `$LOGS` automatically when it is set in the environment; the systemd
> unit above uses an explicit path as a fallback for when `p4_vars` has not
> been sourced.
---
## P4 job field mapping
| P4 field | Source | Notes |
|---|---|---|
| `Job` | JIRA issue key | e.g. `SDP-1234` |
| `Status` | JIRA status name + category | Mapped to jobspec values (see below) |
| `Project` | JIRA project key | Via `JiraProjectMap[]` in config |
| `Severity` | Fixed: `C` | JIRA has no direct equivalent |
| `Type` | JIRA issue type | `Bug` / `Doc` / `Feature` / `Problem` |
| `ReportedBy` | `fields.reporter` email or displayName | |
| `ReportedDate` | `fields.created` | `YYYY/MM/DD` |
| `ModifiedBy` | `fields.reporter` (best available) | JIRA updater not in search API |
| `ModifiedDate` | `fields.updated` | `YYYY/MM/DD` |
| `Description` | First line = JIRA summary; blank line; body | Standard P4 convention |
### Status mapping
| JIRA status (specific name, pattern) | P4 Status |
|---|---|
| matches `*block*` | `blocked` |
| matches `*review*` | `review` |
| matches `*duplicate*` | `duplicate` |
| matches `*obsolete*`, `*wontfix*` | `obsolete` |
| matches `*punt*` | `punted` |
| matches `*done*`, `*resolved*`, `*complete*` | `closed` |
| category = `To Do` / `New` | `open` |
| category = `In Progress` | `inprogress` |
| category = `Done` (fallback) | `closed` |
| (anything else) | `open` |
### Description convention
The first line of the P4 `Description` field doubles as the issue title
(JIRA summary). This is a long-standing P4 Jobs convention on the Public
Depot: tools and views that show only one line of a job's description
show the summary. The full structure is:
```
<JIRA summary line>
<blank line>
<JIRA description body>
```
---
## Resilience design
| Failure scenario | Behaviour |
|---|---|
| JIRA is down | `curl` times out / retries. Watermark NOT advanced. Next run retries from same point. |
| JIRA rate-limits (HTTP 429) | Back-off and retry within current run. |
| Single JIRA page fails | Project aborted; watermark NOT advanced. |
| P4 job upsert fails | Logged; script continues with next issue. Watermark NOT advanced if any failures. |
| P4 is down | `p4 info` preflight fails; script exits immediately. Watermark NOT advanced. |
| Script killed mid-run | flock lock released on exit. Watermark not advanced. Next run re-fetches idempotently. |
| Concurrent invocations | flock prevents overlap; second instance exits immediately. |
---
## Interaction with the JobIncrement trigger
The `JobIncrement.pl` trigger fires on `form-in` events for jobs whose
`Job:` field value is `new`. Since this script always supplies an
explicit job name (the JIRA issue key, e.g. `SDP-1234`), the trigger
exits immediately without any action. Other projects using `JobIncrement`
with their own prefixes are completely unaffected.
---
## State files
| File | Purpose |
|---|---|
| `$StateDir/last_sync_time` | Watermark of last successful poll. Delete to force a full re-sync. |
| `$StateDir/last_sync.lock` | flock advisory lock. Safe to delete when no instance is running. |
| `$StateDir/jira_page_cache/*.json` | Raw JIRA JSON pages; retained 2 days for diagnostics. |
`StateDir` defaults to `/p4/1/tmp/jira_sync` (instance-1 tmp area); override
in the config file if needed.
### Force a full re-sync
```bash
rm /p4/1/tmp/jira_sync/last_sync_time
/p4/common/site/bin/sync_jira_to_p4jobs.sh -v 4
```
---
## JIRA API notes
- Uses `/rest/api/3/search/jql` with `nextPageToken` pagination (the old
`startAt`-based `/rest/api/3/search` was removed from Atlassian Cloud in 2025).
- Authentication: HTTP Basic with email + API token.
- The JQL query is bounded to specific projects and a `updated >=` watermark,
keeping each poll small and incremental.
- `JiraPollDelay` (default 2 s) is inserted after every successful API call.
---
## Obtaining a JIRA API token
1. Log in to https://id.atlassian.com as the service account.
2. **Security** → **API tokens** → **Create API token**.
3. Label it (e.g. `p4-public-depot-sync`).
4. Copy the token immediately (shown only once).
5. Store in `JiraTokenFile` as described in step 3 of Installation.
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #2 | 32919 | C. Thomas Tyler | Tweaked deployment to fit in /p4/common/site folder when deployed. | ||
| #1 | 32918 | C. Thomas Tyler | Moved docs into a docs folder. | ||
| //j2j/dev/bin/sync_jira_to_p4jobs_README.md | |||||
| #1 | 32883 | C. Thomas Tyler | First draft of scripts. | ||