Skip to content

feat(azure): import and auto-deploy Azure DevOps repositories - #636

Open
reach2rv wants to merge 1 commit into
oblien:mainfrom
reach2rv:feature/azure-devops-repo-import
Open

feat(azure): import and auto-deploy Azure DevOps repositories#636
reach2rv wants to merge 1 commit into
oblien:mainfrom
reach2rv:feature/azure-devops-repo-import

Conversation

@reach2rv

@reach2rv reach2rv commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Self-hosted Openship can import, clone, and auto-deploy Azure DevOps repositories as a first-class git source (source: "azure"). Org/project/repo are parsed and stored separately; the PAT is injected only at clone time so it never lands in gitUrl.

Motivation

Self-hosted operators cannot deploy from Azure DevOps today without forging a GitHub URL. Azure’s hierarchy is three-level (org / project / repo), Service Hook IDs are GUIDs, and clone auth is Basic :{PAT} or an Entra bearer token — none of that fits the GitHub two-segment owner/repo model.

This change adds Azure at GitHub parity for self-hosted / desktop only. Cloud SaaS hides the UI and Azure API routes return NOT_SUPPORTED.

Related issue

None. No Azure DevOps issue was found on oblien/openship. This PR adds schema, endpoints, and new behavior; CONTRIBUTING asks for a maintainer-agreed issue before that class of change. Happy to open one and retarget if maintainers want that first.

Changes

packages/core

  • Add "azure" to SOURCE_PROVIDERS.
  • parseGitRepoUrl / buildGitUrl handle:
    • https://dev.azure.com/{org}/{project}/_git/{repo}
    • https://{org}.visualstudio.com/{project}/_git/{repo}
    • git@ssh.dev.azure.com:v3/{org}/{project}/{repo} (parse only; clone is HTTPS)
  • PAT-in-URL is stripped. gitOwner = org, gitProject = Azure project, gitRepo = repo.
  • Azure is a git source in deployment-class. ResourceType includes "azure" but it is not grantable.

packages/db

  • project.git_project (Azure project name).
  • project.webhook_external_id (Service Hook GUID; GitHub still uses integer webhook_id).
  • instance_settings.azure_pat_encrypted / azure_pat_set_at.
  • Migration 0109_azure_devops.sql.
  • No azure_connection table — OAuth uses Better Auth account (providerId = "microsoft").

packages/adapters

  • Clone injects Azure credentials at runtime only: empty username, token as password → https://:{token}@dev.azure.com/....

apps/api

  • Env: AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, optional AZURE_TENANT_ID (default organizations).
  • Better Auth microsoft provider with Azure DevOps scope 499b84ac-1321-427f-aa17-267ca6975798/.default.
  • New module /api/azure (localOnly): status, Entra connect/disconnect, instance PAT, orgs/repos/branches, stack detect, Service Hooks.
  • Webhooks: HTTPS Basic Auth (timing-safe), not SHA-1 HMAC. Push to /api/webhooks/azure auto-deploys matching projects.
  • Project CRUD / prepare / clone-auth use buildGitUrl; tokens never persist in gitUrl.

apps/dashboard

  • Settings → Git → Azure DevOps (self-hosted): Entra connect + encrypted PAT.
  • Library: Azure DevOps tab + Git URL paste.
  • Deploy wizard threads gitProvider: "azure" and gitProject through prepare/ensure.
  • repoSlug v2 encodes { owner, repo, provider, project }; Azure decode rejects a missing project.
  • i18n keys in all locales for library + settings.

Azure is not mounted on github.routes.ts and is not added to GrantableResourceType.

How to use

Self-hosted / desktop only. On cloud SaaS, /api/azure/* returns 400/NOT_SUPPORTED and the Library tab is hidden.

  1. Apply migration 0109_azure_devops.
  2. OAuth (optional): Register a Microsoft Entra app. Set AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, optional AZURE_TENANT_ID (default organizations). Callback is Better Auth microsoft plus dashboard /auth/callback/close.
  3. PAT fallback: Settings → Git → Azure DevOps. PAT scopes: Code (Read) to clone, Service Hooks (Read & Write) for auto-deploy. Stored encrypted; never written into git URLs.
  4. Import: Library → Azure DevOps tab → pick org → deploy. Or Git URL tab: paste https://dev.azure.com/{org}/{project}/_git/{repo}.
  5. Wizard: stack detect / branch / deploy the same as GitHub.
  6. Auto-deploy: enable the project auto-deploy toggle. Openship registers an Azure Service Hook (Basic Auth) to /api/webhooks/azure. The API must be publicly reachable HTTPS.

Clone credential is injected only at clone time (OAuth JWT as Bearer, PAT as Basic :{pat}).

Verification

Targeted unit tests (this branch, before push). Full bun run test, workspace lint, and bun format were not completed locally — bun was not on PATH in the PR shell, and an earlier bun install hit ENOSPC on this machine.

# packages/core/test/project-source.test.ts
# 21 passed — Azure URL parse, PAT strip, buildGitUrl requires project

# packages/adapters/src/runtime/git-clone.test.ts
# 34 passed — Azure host injects empty user + token as password

# apps/dashboard/src/utils/repoSlug.test.ts
# 35 passed — v2 slug encode/decode; Azure rejects missing project

# apps/dashboard/src/i18n/i18n-parity.test.ts
# 3 passed — library/settings key parity (UNTRANSLATED_BASELINE: settings 115, library 26)

# apps/api/src/modules/azure/azure.webhook.test.ts
# 5 passed — Basic Auth accept/reject, missing header, empty secrets, timing-safe length

Manual / SaaS checks still needed (see checklist below).

Checklist

  • One change per PR — one bug, or one agreed feature, with nothing unrelated bundled in
  • The diff is scoped — no reformatting or lint fixes on lines I wasn't otherwise changing
  • A test fails without this change and passes with it (or I explained above why there isn't one)
  • bun run test, bun run --cwd <workspace> lint, and bun format all pass locally
  • I understand every line of this diff and can explain it in review

Self-hosted operators currently cannot deploy from Azure DevOps without
forging a GitHub URL. Parse org/project/repo, store gitProject separately,
and inject the PAT only at clone time so credentials never land in gitUrl.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant