Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions .claude/skills/jira-propose/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# /jira-propose — Propose JIRA ticket changes locally

Create or update a `.jira/*.yml` file to represent a JIRA ticket change. This skill operates **entirely locally** — no JIRA API calls are made. The resulting file is committed as part of a PR and applied to JIRA automatically when the PR merges.

## When to invoke

Invoke `/jira-propose` when:
- A feature, bug fix, or improvement is being built and should be tracked in JIRA
- An existing JIRA ticket needs updates (status, components, description, etc.)
- The user explicitly asks to propose a JIRA ticket for the current work

## Steps

### 1. Gather context

Run these commands in parallel to understand the current work:

```bash
git diff --stat HEAD
git log --oneline -10
git branch --show-current
ls .jira/*.yml 2>/dev/null || echo "(no existing jira files)"
```

Also read any `.jira/*.yml` files that exist, to check for matches.

### 2. Determine issue type

Default to **story** (`id: "7"`). Use **bug** (`id: "1"`) when:
- Branch name contains `fix`, `bug`, `hotfix`, or `patch`
- Commit messages contain `fix:`, `bug:`, `fixes`, `closes`, or `regression`
- The diff clearly addresses a defect rather than adding capability

### 3. Search for a matching existing ticket

Check each `.jira/*.yml` file:
- Compare the filename slug against the branch name and commit messages
- Compare `details.summary` (if present) against the likely ticket title
- If a file matches the current work, **update it** (append to `updates` array)
- If no file matches, **create a new one**

### 4a. New ticket — write `PDCL-XXXX-<short-description>.yml`

Generate a kebab-case short description (3–6 words) from the branch name and commits.

Write `.jira/PDCL-XXXX-<short-description>.yml` with this structure:

```yaml
updates:
- path: /rest/api/2/issue
method: POST
body:
fields:
project:
key: PDCL
issuetype:
id: "7" # Story (use "1" for Bug, "14801" for Documentation)
summary: <concise one-line ticket title>
description: |
<paragraph describing the change, motivation, and business value.
Include: customer names or personas who benefit, what problem this solves,
and the elevator-pitch value statement — why this matters to the product.>
components:
- id: "155901" # AEP Web SDK
customfield_23300: # AEP Web SDK product field
id: "116005"
- path: /rest/api/2/issue/{key}/remotelink
method: POST
body:
globalId: <generate a short unique random hex string, e.g. "a3f7b2c9e1d6">
relationship: mentioned in
object:
url: "{GITHUB_PR_URL}"
title: "{GITHUB_PR_TITLE}"
```

The `globalId` is the idempotency key for this ticket — it must be unique per ticket file so that multiple new tickets can be created in a single PR merge. Generate a random 12-character hex string (e.g. using `Math.random().toString(16).slice(2,14)` mentally). The `{GITHUB_PR_URL}` and `{GITHUB_PR_TITLE}` placeholders are substituted by `apply.mjs` at run time.

**Description guidelines:**
- Lead with who benefits (e.g., "Analytics customers using the Web SDK...")
- Explain the problem being solved
- State the business value / outcome (e.g., "This eliminates the need for manual re-configuration after...")
- Keep to 2–4 sentences

### 4b. Existing ticket — update `updates` array

When a matching file is found, append (or replace if same operation exists) to the `updates` array:

```yaml
updates:
- path: /rest/api/2/issue/{key}
method: PUT
body:
update:
summary:
- set: <new summary>
# Add other fields as needed; always use "set", never "add"/"remove"
```

Use `PUT /rest/api/2/issue/{key}` with `update.field[].set` for field changes.
Use `POST /rest/api/2/issue/{key}/transitions` for status transitions.

### 5. Write the file

- Place it in `.jira/` at the repo root
- Use YAML comments (`#`) to document custom field IDs inline
- Verify the YAML is valid before finishing

### 6. Report back

After writing the file, tell the user:
- The filename created or updated
- The ticket key (PDCL-XXXX for new, PDCL-NNNN for existing)
- The proposed summary
- What will happen when the PR merges (apply.mjs will execute the updates)

## Reference: PDCL custom fields

| Field | ID / value |
|-------|-----------|
| Project key | `PDCL` |
| AEP Web SDK component | `components[].id: "155901"` |
| Documentation component | `components[].id: "157512"` |
| Product field (`customfield_23300`) | `id: "116005"` |
| Story issue type | `issuetype.id: "7"` |
| Bug issue type | `issuetype.id: "1"` |
| Documentation issue type | `issuetype.id: "14801"` |
| JIRA base URL | `https://jira.corp.adobe.com` |

## Constraints

- **No JIRA API calls** — all information comes from git state, context, and existing `.jira/` files
- All `update` operations must use `set` (never `add`/`remove`) to ensure idempotency
- New tickets must have a `POST /rest/api/2/issue` as the first entry in `updates`
- File must be valid YAML
- Do not populate the `details` section — that is written by `fetch.mjs` after apply
93 changes: 92 additions & 1 deletion .github/workflows/version-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,95 @@ concurrency:
cancel-in-progress: false

jobs:
# Applies any pending .jira/*.yml changes to JIRA on every push to main.
apply-jira:
runs-on: ubuntu-latest
environment: Production
permissions:
contents: write
pull-requests: write
env:
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 2
ssh-key: ${{ secrets.ALLOY_BOT_GITHUB_SSH_PRIVATE_KEY }}
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v5
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile

- name: Detect changed .jira files
id: jira-files
run: |
changed=$(git diff HEAD^1 HEAD --name-only -- '.jira/*.yml' | tr '\n' ' ')
echo "files=$changed" >> "$GITHUB_OUTPUT"
if [ -n "$changed" ]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi

- name: Get PR context
id: pr-context
if: steps.jira-files.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pr_info=$(gh pr list --state merged --base main --limit 5 --json number,url,title \
--jq 'map(select(.number != null)) | first')
pr_number=$(echo "$pr_info" | jq -r '.number // ""')
pr_url=$(echo "$pr_info" | jq -r '.url // ""')
pr_title=$(echo "$pr_info" | jq -r '.title // ""')
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT"
echo "pr_title=$pr_title" >> "$GITHUB_OUTPUT"

- name: Apply JIRA changes and fetch updated state
id: apply-jira
if: steps.jira-files.outputs.has_changes == 'true'
env:
GITHUB_PR_URL: ${{ steps.pr-context.outputs.pr_url }}
GITHUB_PR_TITLE: ${{ steps.pr-context.outputs.pr_title }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
applied_tickets=""
for file in ${{ steps.jira-files.outputs.files }}; do
ticket_key=$(node scripts/jira/process.js "$file")
[ -n "$ticket_key" ] && applied_tickets="$applied_tickets $ticket_key"
done
echo "tickets=$applied_tickets" >> "$GITHUB_OUTPUT"

- name: Commit refreshed .jira files
if: steps.jira-files.outputs.has_changes == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A -- '.jira/'
if ! git diff --cached --quiet; then
git commit -m "chore: refresh JIRA details [skip ci]"
git push origin HEAD:main
fi

- name: Post PR comment with JIRA tickets
if: >
steps.jira-files.outputs.has_changes == 'true' &&
steps.apply-jira.outputs.tickets != '' &&
steps.pr-context.outputs.pr_number != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tickets="${{ steps.apply-jira.outputs.tickets }}"
base_url="https://jira.corp.adobe.com/browse"
body="**JIRA tickets updated by this PR:**"$'\n'
for key in $tickets; do
body="$body"$'\n'"- [$key]($base_url/$key)"
done
gh pr comment ${{ steps.pr-context.outputs.pr_number }} --body "$body"

# Cheap gate so the Production approval is only requested when there's
# actual release work (changesets exist) or the operator forced a run.
detect:
Expand All @@ -32,7 +121,7 @@ jobs:
fi

publish:
needs: detect
needs: [detect, apply-jira]
if: needs.detect.outputs.proceed == 'true' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
environment: Production
Expand Down Expand Up @@ -66,6 +155,8 @@ jobs:
pnpm changeset version
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Pull any commits made by apply-jira before pushing (e.g. JIRA detail refresh)
git pull --rebase origin main
git add -A
if ! git diff --cached --quiet; then
git commit -m "chore: publish [skip ci]"
Expand Down
Empty file added .jira/.gitkeep
Empty file.
98 changes: 98 additions & 0 deletions .jira/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# .jira/ — Version-controlled JIRA ticket management

YAML files in this directory represent pending JIRA changes. When a PR merges, the build workflow applies each file to JIRA and replaces it with a refreshed snapshot.

## File naming convention

| File type | Name format | Example |
|-----------|-------------|---------|
| Existing ticket | `{PROJECT}-{TICKET#}-{short-title}.yml` | `PDCL-1234-support-for-adcloud.yml` |
| New ticket (not yet in JIRA) | `{PROJECT}-XXXX-{short-description}.yml` | `PDCL-XXXX-add-identity-map-support.yml` |

Short titles and descriptions use kebab-case, 3–6 words.

## YAML schema

```yaml
# Optional read-only snapshot of the ticket's current JIRA state.
# Populated by fetch.mjs; ignored by apply.mjs.
details:
key: PDCL-1234
summary: Support for AdCloud
status: { name: "In Progress" }
# ... other non-null fields from JIRA

# Optional ordered array of idempotent REST calls to apply on merge.
# Omit if there are no pending changes.
updates:
- path: /rest/api/2/issue/PDCL-1234
method: PUT
body:
update:
summary:
- set: "Updated title"
customfield_23300: # AEP Web SDK product field
- set: { id: "116005" }
```

### `details` section

- Written by `fetch.mjs`; never modified manually
- Read-only snapshot; apply.mjs ignores it entirely
- Null and empty-array fields are omitted
- String fields longer than 500 chars are truncated with `...`

### `updates` section

- Ordered array of `{ path, method, body }` REST call objects
- `body` is YAML and is serialized to JSON before sending to JIRA
- All field updates must use `set` operations (never `add`/`remove`) to ensure idempotency
- For new tickets: first entry is a `POST /rest/api/2/issue` with `fields` (not `update`)
- Absent or empty `updates` → apply.mjs prints the key and exits 0 without any API calls

## Custom field reference (PDCL project)

| Field | ID | Value |
|-------|----|-------|
| AEP Web SDK component | `components[].id` | `"155901"` |
| Documentation component | `components[].id` | `"157512"` |
| AEP Web SDK product (`customfield_23300`) | `id` | `"116005"` |
| Issue type: Story | `issuetype.id` | `"7"` |
| Issue type: Bug | `issuetype.id` | `"1"` |
| Issue type: Documentation | `issuetype.id` | `"14801"` |

## End-to-end flow

```
Developer CI (on merge to main)
───────── ─────────────────────
/jira-propose → apply.mjs <file>
↓ writes .jira/*.yml ↓ calls JIRA REST API
PR review ↓ creates remote link (repo-{PR#})
↓ approves JIRA changes delete file
Merge PR fetch.mjs <key> <new-file>
↓ writes refreshed details
skip-ci commit
```

1. **Propose** — Run `/jira-propose` in Claude Code. Reads git diff and context, creates or updates a `.jira/*.yml` file locally. No JIRA API calls.
2. **Review** — The `.jira/` file diff appears in the PR. Reviewers can inspect and amend JIRA changes before they land.
3. **Apply** — On merge, `apply.mjs` executes each `updates` entry in order. For new (`XXXX`) tickets it creates the ticket first (idempotent: checks whether a remote link with `globalId: repo-{PR#}` already exists).
4. **Fetch** — After apply succeeds, the XXXX file is deleted and `fetch.mjs` writes a fresh file named with the real ticket key. A `[skip ci]` commit lands both JIRA and changeset changes.

## Scripts

```bash
# Apply a ticket file's updates to JIRA (CI usage)
JIRA_API_TOKEN=... GITHUB_PR_URL=... GITHUB_PR_TITLE=... \
node scripts/jira/apply.mjs .jira/PDCL-1234-my-feature.yml

# Dry-run: see planned API calls without making them
node scripts/jira/apply.mjs --dry-run .jira/PDCL-1234-my-feature.yml

# Fetch current JIRA state into a file
JIRA_API_TOKEN=... node scripts/jira/fetch.mjs PDCL-1234 .jira/PDCL-1234-my-feature.yml

# Dry-run: see what would be written
node scripts/jira/fetch.mjs --dry-run PDCL-1234 .jira/PDCL-1234-my-feature.yml
```
45 changes: 45 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Alloy — Claude Code guide
Comment thread
jonsnyder marked this conversation as resolved.
Outdated

## JIRA ticket management

JIRA changes are version-controlled as YAML files in `.jira/`. No JIRA API calls are made locally.

### Workflow

1. **Propose** — Run `/jira-propose` to create or update a `.jira/*.yml` file based on current git changes and context.
2. **Review** — The `.jira/` diff appears in the PR. Reviewers approve JIRA changes alongside code changes.
3. **Apply** — On merge to `main`, the `apply-jira` CI job executes each file's `updates` array against the JIRA REST API, creates a remote link back to the PR, and posts a PR comment listing updated tickets.
4. **Fetch** — After apply, the XXXX file is deleted and `fetch.js` writes a fresh file with the real ticket key and current JIRA state. A `[skip ci]` commit lands these changes.

### File naming

- Existing ticket: `PDCL-1234-short-title.yml`
- New ticket (not yet in JIRA): `PDCL-XXXX-short-description.yml`

### Scripts

```bash
# See planned JIRA calls without making them (safe for local use)
node scripts/jira/apply.js --dry-run .jira/PDCL-1234-my-feature.yml

# Fetch current JIRA state into a file
JIRA_API_TOKEN=<token> node scripts/jira/fetch.js PDCL-1234 .jira/PDCL-1234-my-feature.yml
```

### CI environment variables (Production GitHub Actions environment)

| Variable | Required for |
|----------|-------------|
| `JIRA_API_TOKEN` | apply.js and fetch.js API calls |
| `ALLOY_BOT_GITHUB_SSH_PRIVATE_KEY` | Pushing skip-ci commits to main |

### Custom fields (PDCL project)

| Field | Value |
|-------|-------|
| AEP Web SDK component | `components[].id: "155901"` |
| Product (`customfield_23300`) | `id: "116005"` |
| Story `issuetype.id` | `"7"` |
| Bug `issuetype.id` | `"1"` |

See [`.jira/README.md`](./.jira/README.md) for the full schema reference.
Loading