create-spec-driven-app v0.7.0

Jira and Azure Boards#

Goal: every requirement in the matrix has exactly one issue on the board, and the two never quietly disagree.

csda alm sync creates an issue for each unlinked REQ, closes the issue when its requirement reaches a done status, and reports drift — an issue closed while its requirement is still open — without resolving it for you.

The ALM is a mirror, not a source of truth. The spec and the matrix are the contract; an issue reflects a requirement, it never defines one. That is why sync closes issues but never reopens requirements, and why drift is reported rather than reconciled.

Configure#

alm.config.yaml at the project root. Tokens never live in this file — only the name of the environment variable that holds them, so the file is safe to commit and CI supplies the secret.

alm_version: 1
provider: jira            # jira | azure
base_url: https://acme.atlassian.net
project_key: HIE
token_env: JIRA_TOKEN     # env var holding the API token
user_env: JIRA_USER       # jira only: env var holding the account email

Which keys a provider reads is the provider's own declaration, and they differ:

ProviderRequiredOptional
jirabase_url, project_key, token_envuser_env, issue_type
azurebase_url, project_key, token_envissue_type, done_state
githubrepo, token_envbase_url

Three asymmetries worth knowing, because the first two used to fail silently:

  • done_state is Azure-only. Jira's workflows are per-project, so "done" there is a status category and closing means finding a transition into it. Setting done_state on a Jira project does nothing — and sync now says so before its first request.
  • user_env is Jira-only, because Jira Cloud's Basic auth pairs the token with an account email. It defaults to JIRA_USER when omitted.
  • GitHub takes repo, not project_key, and its base_url is optional. Its unit is a repository — repo: acme/widgets — and github.com has one API host, so only GitHub Enterprise Server needs a base URL. It has no issue_type (GitHub has labels) and no done_state (an issue is open or closed).

Any key that the configured provider does not read is reported as a warning with a fix, naming the provider that would have read it.

Closed is not always done, on GitHub#

GitHub records why an issue was closed. Closing as not planned means the team decided against the work; closing as completed means it happened. Only the second reads as done here.

Collapsing both would tell traceability.md that a requirement was delivered when it had in fact been abandoned. Instead a not-planned close leaves the requirement open, and the disagreement shows up as drift for a person to settle — which is what ADR-0021 asks for: the board is a mirror, and it never advances a requirement on its own.

Two tiers: core and community#

jira, azure and github are core — maintained in this repository and exercised by the conformance kit on every CI run. Anything else is resolved by package name:

# alm.config.yaml
provider: npm:csda-alm-youtrack
npm install --save-dev csda-alm-youtrack   # you install it; csda never does

The split is not a preference. Every connector is somebody else's API, changing on their schedule: carrying six here would tie this repository's release cadence to six vendors, and carrying none would make the tool useless to a team on YouTrack. So the port is the contract, and anyone can implement it.

What you are agreeing to#

A community provider is code that runs in your process, and it is handed your ALM credential — a connector cannot talk to a board without one. The trust model is exactly that of a devDependency, and it deserves the same scrutiny: read it, pin it, and treat an update like any other dependency update.

Three things the CLI does so that it is no worse than a devDependency:

  • Nothing is ever installed automatically. The package must already be in your node_modules. Editing a config file can never fetch and run new code — adding a provider stays a dependency decision, made deliberately.
  • The name must be a package name. No paths, no URLs, no ... A committed config file cannot be made to load an arbitrary file from your disk.
  • The module is checked against the port before anything is called. A package that is not a provider, or one that claims a capability it has no method for, fails with a diagnostic naming the problem — not with a stack trace from inside sync, halfway through a run.

Writing one#

Export an object satisfying AlmProviderid, label, config, capabilities and a create(cfg, fetchImpl) factory. Taking fetchImpl is what lets the conformance kit exercise a provider against recorded responses, with no network; the three core providers are worth reading as examples, and scripts/alm/port.ts documents every field and why it exists.

Declare capabilities honestly. capabilities.listIssues: false is a supported answer — azure gives it — and it makes alm pull degrade with a message rather than fail at the first request.

Bringing work in: alm pull#

The request every team makes after seeing sync is the other direction — the product owner opens the ticket, make it appear in the repo. It does, as a change, never as a matrix row:

csda alm pull --label spec-driven            # one change per labelled issue
csda alm pull --label spec-driven --dry-run  # see what it would open

Each pulled issue becomes docs/specs/changes/alm-<key>/ with a proposal quoting the issue verbatim — not summarised, so a reviewer reads the reporter's words and not this tool's paraphrase — and a delta carrying one requirement.

The scenario is left unwritten, and change validate refuses the change until you write it. That is the design, not a gap. A ticket has a title, a description and a status; it has no executable acceptance criterion. Generating Gherkin from prose would invent the one thing the spec exists to pin down, and the gate would then be checking fiction. The empty scenario marks the only work that cannot be automated — yours, or the spec-author role's via csda change author.

Pulling twice is safe: a change that already exists is skipped, so edits you have started are never overwritten.

Searching is a declared capability. github and jira can filter by label; azure cannot, because Azure searches with WIQL — a different shape entirely — and says so rather than failing at the first request.

Run#

export JIRA_TOKEN=…  JIRA_USER=…
csda alm sync --dry-run     # plan only: no issue is created or closed
csda alm sync               # do it, and write .specops/alm-map.json
csda alm status             # the REQ ↔ issue mapping, no network
csda alm link REQ-007 HIE-42   # adopt an issue that already exists

.specops/alm-map.json is the mapping and belongs in git: it is what makes sync idempotent, and what lets a second machine pick up where the first left off. sync exits 1 when it finds drift, so it works as a scheduled CI job.

Adding a provider#

A provider is one module under scripts/alm/providers/ plus a row in that directory's index.ts. It declares which config keys it reads and which operations it supports, and implements three methods — createIssue, getIssueStatus, closeIssue. tests/unit/alm-conformance.test.ts then runs the same suite against it as against the two that ship, from a recorded fixture in tests/fixtures/alm/<id>.json; a registered provider without one fails.


Next#