Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
073d7a8
Add workflow to trigger wheel releases
dkirov-dd Feb 17, 2026
3cf8491
Fix code injection vulnerability in trigger workflow
dkirov-dd Feb 17, 2026
1b513c1
Use dd-octo-sts for secure cross-repo authentication
dkirov-dd Feb 17, 2026
5303ac1
Pin actions/github-script to specific commit SHA
dkirov-dd Feb 17, 2026
8758b2a
Add pull_request trigger for testing
dkirov-dd Feb 17, 2026
68eacef
Add id-token permission for OIDC authentication
dkirov-dd Feb 17, 2026
93b4d93
Update dd-octo-sts policy name
dkirov-dd Feb 17, 2026
cdb39bf
Use different dd-octo-sts policies for PR vs production
dkirov-dd Feb 17, 2026
ddd1ea1
trigger
dkirov-dd Feb 17, 2026
2ff5cf2
Remove master from pull_request trigger branches
dkirov-dd Feb 17, 2026
f6da664
Use workflow_dispatch for PR testing
dkirov-dd Feb 17, 2026
aaea6b2
Fix pull_request trigger for testing
dkirov-dd Feb 17, 2026
db5ecd6
Add dispatch-release workflow with chunking support
dkirov-dd Feb 20, 2026
e667643
Add chunking test with 4 integrations and batch size of 2
dkirov-dd Feb 20, 2026
9bfcf3d
Rename workflow call
dkirov-dd Feb 20, 2026
b80c8b1
Rename redis to redisdb
dkirov-dd Feb 20, 2026
31ffc91
Merge branch 'master' into dk/test-release-trigger
dkirov-dd Feb 24, 2026
4b174ec
Use windows_performance_counters and ibm_mq as test integrations
dkirov-dd Feb 24, 2026
05fe7da
Move logic to Python scripts
dkirov-dd Mar 4, 2026
ce43ee3
Improve logic separation
dkirov-dd Mar 4, 2026
7a13da8
Rename trigger job
dkirov-dd Mar 4, 2026
10efada
Add environment condition for workflow dispatch
dkirov-dd Mar 4, 2026
70d8f05
Dispatch release even if nothing was tagged (use the HEAD tags)
dkirov-dd Mar 4, 2026
9bffcc5
fix(release): fix integration glob path and validate unknown integrat…
dkirov-dd Mar 4, 2026
a477a91
feat(release): batch integration dispatches
dkirov-dd Mar 4, 2026
d643394
Hard code batch size
dkirov-dd Mar 4, 2026
a1ccdbd
Use dry-run mode for now
dkirov-dd Mar 5, 2026
978281e
Add dry-run to trigger workflow
dkirov-dd Mar 5, 2026
c3eee73
Change dry-run description
dkirov-dd Mar 5, 2026
acae08d
Replace dry-run with target
dkirov-dd Mar 5, 2026
8f22903
Merge branch 'master' into dk/test-release-trigger
dkirov-dd Mar 6, 2026
7d62b99
Merge branch 'master' into dk/test-release-trigger
dkirov-dd Mar 6, 2026
fc4f016
Add validation step to dispatch
dkirov-dd Mar 6, 2026
1e3235e
Use sys.exit instead of raising error
dkirov-dd Mar 6, 2026
1072bf5
Simplify version checking
dkirov-dd Mar 6, 2026
d2856bb
feat(release): rename integration→package and add dry-run mode
dkirov-dd Mar 9, 2026
5c42127
fix(release): restore dry-run input to trigger-release.yml
dkirov-dd Mar 9, 2026
b73e121
Add ddev-version input parameter
dkirov-dd Mar 10, 2026
fc16d12
TEST trigger dry-run release
dkirov-dd Mar 11, 2026
3e2db32
Fix name detection
dkirov-dd Mar 11, 2026
836f203
Add ddev configuration step
dkirov-dd Mar 11, 2026
9654f11
Merge branch 'master' into dk/test-release-trigger
dkirov-dd Mar 11, 2026
3f341ca
TEST Use test sts policy and delete datadog_checks_base changelog.d
dkirov-dd Mar 11, 2026
d50aec1
TEST Use dry-run mode for testing
dkirov-dd Mar 11, 2026
51e8d5d
chore(release): improve detect_packages output verbosity
dkirov-dd Mar 11, 2026
fcc40ed
chore(release): improve dispatch output and add run tracking link
dkirov-dd Mar 11, 2026
626ca7f
chore(release): improve validate_release output verbosity
dkirov-dd Mar 11, 2026
6c7b6bf
Docs changes
dkirov-dd Mar 11, 2026
c787953
Add job summaries
dkirov-dd Mar 11, 2026
f920fde
chore(release): fix formatting issues and f-string inconsistencies
dkirov-dd Mar 11, 2026
1ac9ff4
Prevent printing released integrations twice
dkirov-dd Mar 11, 2026
86de6e7
EXPERIMENTAL rework summary format
dkirov-dd Mar 11, 2026
5b70418
Revert TEST commits
dkirov-dd Mar 11, 2026
118a5e8
refactor(release-scripts): modularize release pipeline scripts and ad…
dkirov-dd Mar 12, 2026
ced3eea
fix(release-scripts): address post-review issues
dkirov-dd Mar 12, 2026
98cfd3c
test(release-scripts): trim redundant tests (63 → 48)
dkirov-dd Mar 12, 2026
7c7ea05
fix(release-scripts): address review feedback
dkirov-dd Mar 12, 2026
4874de3
TEST: add dry-run workflow triggered on pull_request
dkirov-dd Mar 12, 2026
5b6460d
Revert "TEST: add dry-run workflow triggered on pull_request"
dkirov-dd Mar 12, 2026
16f348a
fix(release-scripts): address review feedback
dkirov-dd Mar 12, 2026
7b629f9
Update .github/workflows/test-release-scripts.yml
dkirov-dd Mar 12, 2026
69534a0
Improve ddev-version input parsing
dkirov-dd Mar 13, 2026
742aa8c
Remove conftest
dkirov-dd Mar 13, 2026
d95ef38
refactor(release-scripts): decouple dispatch functions from global co…
dkirov-dd Mar 13, 2026
8408c89
refactor(release-scripts): clean up dispatch_in_batches and build_sum…
dkirov-dd Mar 13, 2026
b87b54b
feat(release-scripts): add is_stable_release dispatch context
dkirov-dd Mar 16, 2026
52d0a1a
refactor(trigger): replace Python branch detection with shell allowlist
dkirov-dd Mar 16, 2026
b292c3c
refactor(dispatch): add _urlopen wrapper for mockable HTTP boundary
dkirov-dd Mar 16, 2026
ef3e999
test(release-scripts): improve test clarity and parametrize bool env …
dkirov-dd Mar 16, 2026
7cdfb17
chore(workflows): pin pytest, set prod default, remove validated TODOs
dkirov-dd Mar 16, 2026
25f9cf4
docs(workflows): add cross-repo comments to trigger and dispatch work…
dkirov-dd Mar 16, 2026
c737aa6
test(packages): assert exact mode strings; pin ddev to 14.3.2
dkirov-dd Mar 16, 2026
aeee132
docs(dispatch-release): clarify ddev-version input is pinned by default
dkirov-dd Mar 16, 2026
634c9a7
test(release-scripts): add tests for entry-point scripts
dkirov-dd Mar 17, 2026
ea783e6
fix(release-scripts): write summary on stable-on-pre-release failure;…
dkirov-dd Mar 17, 2026
6dc1bfb
refactor(_release/summary): redesign label logic and rename dispatche…
dkirov-dd Mar 17, 2026
7e03c56
refactor(_release/summary): replace dict lookup with if-chain in _row…
dkirov-dd Mar 17, 2026
4a28302
test(_release): strengthen assertions and cover ValueError path
dkirov-dd Mar 17, 2026
50e0fb6
TEST test(workflows): add dry-run PR trigger for release pipeline smo…
dkirov-dd Mar 17, 2026
6fb804d
Revert "TEST test(workflows): add dry-run PR trigger for release pipe…
dkirov-dd Mar 17, 2026
4abf30b
Merge branch 'master' into dk/test-release-trigger
dkirov-dd Mar 17, 2026
96a5a2f
test(_release): use full mock.call assertions instead of index-based …
dkirov-dd Mar 17, 2026
552db17
Rename manual to selected
dkirov-dd Mar 17, 2026
a98174d
Use kwargs for resolve_pagages tests
dkirov-dd Mar 17, 2026
5e6db36
test(_release): replace call_args access with full mock_calls assertions
dkirov-dd Mar 17, 2026
5b19ea7
Fix test
dkirov-dd Mar 17, 2026
dd6dab4
fix(_release): address PR review findings for release trigger workflow
dkirov-dd Mar 18, 2026
8bd382d
chore: remove .agint-review worktrees from index, add to .gitignore
dkirov-dd Mar 18, 2026
e2da099
Fix .gitignore to include root.json
dkirov-dd Mar 19, 2026
9e692bc
chore(_release): rename workflow files to release-* prefix
dkirov-dd Mar 20, 2026
9f68206
chore: remove .agint-review worktrees from index again
dkirov-dd Mar 20, 2026
519d8d3
refactor(_release): merge tag/detect/validate into release_prepare.py
dkirov-dd Mar 20, 2026
580963c
fix(_release): address PR review findings
dkirov-dd Mar 20, 2026
ea5bd72
refactor(_release): read dry_run from env var in release_dispatch.py
dkirov-dd Mar 20, 2026
b91002a
test(_release): add PR-based dry-run trigger for release dispatch
dkirov-dd Mar 20, 2026
f695050
Revert "test(_release): add PR-based dry-run trigger for release disp…
dkirov-dd Mar 20, 2026
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
99 changes: 99 additions & 0 deletions .github/workflows/release-dispatch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Dispatch Release

# Reusable workflow — intended to be called by a release-trigger workflow in an
# integration repository (integrations-core, integrations-extras, marketplace).

on:
workflow_call:
inputs:
source-repo:
description: "Source repository name (e.g. integrations-core, integrations-extras, marketplace)"
required: false
type: string
packages:
description: >-
Packages to release. Accepts a JSON array (e.g. '["postgres","datadog_checks_base"]'),
'all' to release every Python package in the repo, or omit to auto-detect from new tags.
required: false
type: string
source-repo-ref:
description: "Commit SHA or ref to build from"
required: false
type: string
target:
description: "Target environment (dev = no tag push, dev S3; prod = push tags, prod S3)"
required: false
type: string
default: prod # callers (e.g. release-trigger.yml) typically override this
dry-run:
description: >-
When true, print what would be released and where without pushing tags
or triggering downstream wheel builds.
required: false
type: boolean
default: false
ddev-version:
description: "ddev version, pinned by default."
required: false
type: string
is-stable-release:
description: "'true' for master/X.X.x (blocks pre-releases), 'false' for alpha/beta/rc (blocks stable). Unset defaults to stable behavior."
required: false
type: string

permissions:
id-token: write
contents: write # ddev needs to push tags

jobs:
dispatch:
name: Tag releases and dispatch wheel builds
runs-on: ubuntu-latest
environment: release

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0 # ddev needs full tag history

- name: Install ddev
run: pip install "ddev${{ inputs.ddev-version && format('=={0}', inputs.ddev-version) || '==14.3.2' }}"

- name: Configure ddev
env:
SOURCE_REPO: ${{ inputs.source-repo || 'integrations-core' }}
run: |
REPO_SHORT="${SOURCE_REPO#integrations-}"
ddev config set upgrade_check false
ddev config set repos.${REPO_SHORT} .
ddev config set repo ${REPO_SHORT}

- name: Prepare dispatch
id: prepare
env:
TARGET: ${{ inputs.target }}
DRY_RUN: ${{ inputs.dry-run }}
SELECTED_PACKAGES: ${{ inputs.packages }}
SOURCE_REPO: ${{ inputs.source-repo || 'integrations-core' }}
REF: ${{ inputs.source-repo-ref || github.sha }}
IS_STABLE_RELEASE: ${{ inputs.is-stable-release }}
run: python .github/workflows/scripts/release_prepare.py

- name: Get GitHub token via dd-octo-sts
if: steps.prepare.outputs.has_packages == 'true' && !inputs.dry-run
id: octo-sts
uses: DataDog/dd-octo-sts-action@08f2144903ced3254a3dafec2592563409ba2aa0 # v1.0.1
with:
scope: DataDog/agent-integration-wheels-release
policy: integrations-core.dispatch-wheel-builds

- name: Dispatch release
if: steps.prepare.outputs.has_packages == 'true'
env:
GH_TOKEN: ${{ steps.octo-sts.outputs.token }}
PACKAGES: ${{ steps.prepare.outputs.packages }}
SOURCE_REPO: ${{ inputs.source-repo || 'integrations-core' }}
REF: ${{ inputs.source-repo-ref || github.sha }}
TARGET: ${{ inputs.target }}
DRY_RUN: ${{ inputs.dry-run }}
run: python .github/workflows/scripts/release_dispatch.py
79 changes: 79 additions & 0 deletions .github/workflows/release-trigger.yml
Comment thread
iliakur marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Trigger Wheel Builds

# Thin wrapper — all tagging and dispatch logic lives in release-dispatch.yml.
# The pipeline can build wheels for any Python package in the repo.
# Equivalent trigger workflows exist in integrations-extras and marketplace.

on:
push:
branches:
- master
- "[0-9]+.[0-9]+.x"
- "alpha/*"
- "beta/*"
- "rc/*"
paths:
- "*/CHANGELOG.md"
- "*/datadog_checks/*/__about__.py"
workflow_dispatch:
inputs:
packages:
description: >-
Packages to release. JSON array (e.g. '["postgres","datadog_checks_base"]'),
'all' to release every Python package in the repo, or omit to auto-detect from new tags.
required: false
type: string
source-repo-ref:
description: "Commit SHA or ref to build from"
required: true
type: string
target:
description: "Target environment (dev or prod)"
required: false
type: choice
options:
- dev
- prod
default: dev
dry-run:
description: "Print what would be released without pushing tags or starting builds"
required: false
type: boolean
default: false
ddev-version:
description: "ddev version, pinned by default."
required: false
type: string

jobs:
context:
name: Detect release context
runs-on: ubuntu-latest
outputs:
is-stable-release: ${{ steps.detect.outputs.is-stable-release }}
steps:
- id: detect
# Sets is-stable-release based on the branch: true for master/X.Y.x, false otherwise.
# Manual runs on master get "true", which blocks pre-release packages — conservative and intentional.
run: |
if [[ "$GITHUB_REF" =~ ^refs/heads/(master|[0-9]+\.[0-9]+\.x)$ ]]; then
echo "is-stable-release=true" >> "$GITHUB_OUTPUT"
else
echo "is-stable-release=false" >> "$GITHUB_OUTPUT"
fi

dispatch:
name: Release
needs: context
uses: ./.github/workflows/release-dispatch.yml
with:
source-repo: integrations-core
packages: ${{ inputs.packages || '' }}
source-repo-ref: ${{ github.event_name == 'workflow_dispatch' && inputs.source-repo-ref || github.sha }}
target: ${{ inputs.target || 'dev' }}
dry-run: ${{ inputs.dry-run || false }}
ddev-version: ${{ inputs.ddev-version || '' }}
is-stable-release: ${{ needs.context.outputs.is-stable-release }}
permissions:
id-token: write
contents: write
1 change: 1 addition & 0 deletions .github/workflows/scripts/_release/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TARGET_REPO = "DataDog/agent-integration-wheels-release"
102 changes: 102 additions & 0 deletions .github/workflows/scripts/_release/dispatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""HTTP dispatch logic for repository_dispatch events."""
import http.client
import json
import ssl
import sys
import time
import urllib.error
import urllib.request

from . import TARGET_REPO

BATCH_SIZE = 200
DISPATCH_URL = f"https://api.github.com/repos/{TARGET_REPO}/dispatches"
MAX_ATTEMPTS = 5


class DispatchError(Exception):
"""Raised when a dispatch request fails after all retry attempts."""


def build_payload(batch: list[str], source_repo: str, ref: str, target: str) -> dict:
"""Return the ``repository_dispatch`` payload for one batch."""
return {
"event_type": "build-wheels",
"client_payload": {
"packages": batch,
"source_repo": source_repo,
"source_repo_ref": ref,
"target": target,
},
}


def _urlopen(req: urllib.request.Request) -> http.client.HTTPResponse:
"""Thin urllib wrapper — exists so tests can patch it without touching stdlib."""
return urllib.request.urlopen(req)


def send_dispatch(
payload: dict,
token: str,
*,
dispatch_url: str = DISPATCH_URL,
max_attempts: int = MAX_ATTEMPTS,
) -> None:
"""POST a single ``repository_dispatch`` event to the wheels-release repo.

Retries up to ``max_attempts`` times on 5xx errors with exponential backoff.
Raises ``DispatchError`` on 4xx errors or after exhausting retries.
"""
req = urllib.request.Request(
dispatch_url,
data=json.dumps(payload).encode(),
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
method="POST",
)
for attempt in range(1, max_attempts + 1):
try:
with _urlopen(req) as resp:
print(f" Dispatched: HTTP {resp.status}")
return
except (urllib.error.HTTPError, urllib.error.URLError) as e:
if isinstance(e, urllib.error.URLError):
if isinstance(e.reason, ssl.SSLError):
raise DispatchError(f"SSL error (non-retriable): {e}") from e
body = str(e)
code = 503
else:
body = e.read().decode()
code = e.code
if code < 500 or attempt == max_attempts:
print(f"HTTP {code}: {body}", file=sys.stderr)
raise DispatchError(f"HTTP {code}: {body}")
print(f" HTTP {code} on attempt {attempt}/{max_attempts}, retrying...", file=sys.stderr)
time.sleep(2**attempt)


def dispatch_in_batches(
packages: list[str],
source_repo: str,
ref: str,
target: str,
token: str,
batch_size: int = BATCH_SIZE,
) -> None:
"""Dispatch all packages to the wheels-release repo, batching if needed."""
if batch_size <= 0:
raise ValueError("batch_size must be > 0")
num_packages = len(packages)
if num_packages == 0:
return
total_batches = (num_packages + batch_size - 1) // batch_size
for batch_num, start in enumerate(range(0, num_packages, batch_size), 1):
end = min(start + batch_size, num_packages)
current_batch = packages[start:end]
print(f"\nBatch {batch_num}/{total_batches}:")
print("\n".join(f" - {name}" for name in current_batch))
send_dispatch(build_payload(current_batch, source_repo, ref, target), token)
36 changes: 36 additions & 0 deletions .github/workflows/scripts/_release/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""GitHub Actions I/O helpers."""
import os
from pathlib import Path


def set_outputs(**kwargs: str) -> None:
"""Write key=value pairs to GITHUB_OUTPUT."""
path = os.environ.get("GITHUB_OUTPUT")
if path:
with Path(path).open("a") as f:
for key, value in kwargs.items():
f.write(f"{key}={value}\n")


def write_summary(content: str) -> None:
"""Append markdown content to GITHUB_STEP_SUMMARY."""
path = os.environ.get("GITHUB_STEP_SUMMARY")
if path:
with Path(path).open("a") as f:
f.write(content + "\n")


def parse_bool_env(name: str, default: bool = False) -> bool:
"""Parse a boolean environment variable.

Accepts 'true'/'1'/'yes' as True and 'false'/'0'/'no' as False (case-insensitive).
Returns ``default`` when the variable is absent or empty.
"""
val = os.environ.get(name, "").strip().lower()
if not val:
return default
if val in ("true", "1", "yes"):
return True
if val in ("false", "0", "no"):
return False
return default
69 changes: 69 additions & 0 deletions .github/workflows/scripts/_release/packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Package detection and resolution logic."""
import json
import re
import subprocess
from pathlib import Path

_VERSION_SUFFIX_RE = re.compile(r"-\d+\.\d+\.\d+.*$")


def get_all_packages(root: Path = Path(".")) -> list[str]:
"""Return sorted list of all Python packages found under *root*."""
return sorted(
{p.parent.parent.parent.name for p in root.glob("*/datadog_checks/*/__about__.py")}
)


def get_tags_at_head() -> list[str]:
"""Return the list of git tags pointing at HEAD."""
try:
return subprocess.check_output(["git", "tag", "--points-at", "HEAD"], text=True).splitlines()
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Failed to get git tags at HEAD: {e}") from e


def detect_from_tags(tags: list[str]) -> list[str]:
"""Return sorted unique package names extracted from release tags.

Tags are expected in the form ``<package>-X.Y.Z[...]``.
The version suffix is stripped to recover the package name.
"""
return sorted({_VERSION_SUFFIX_RE.sub("", t) for t in tags if t.strip()})


def resolve_packages(
selected: str,
all_packages: list[str],
head_tags: list[str] | None = None,
) -> tuple[list[str], str]:
"""Resolve the list of packages to release.

Resolution order:
- ``'all'`` / ``'ALL'`` → every package in the repo
- JSON array → use the provided list verbatim
- empty string → auto-detect from git tags at HEAD

Returns ``(packages, mode_description)``.
Raises ``ValueError`` on invalid input or unknown package names.
"""
selected = selected.strip()

if selected.lower() == "all":
return all_packages, f"all ({len(all_packages)} packages in repo)"

if selected:
try:
packages = json.loads(selected)
except json.JSONDecodeError as e:
raise ValueError(f"SELECTED_PACKAGES is not valid JSON: {e}") from e
mode = f"manual ({selected})"
else:
tags = head_tags if head_tags is not None else get_tags_at_head()
packages = detect_from_tags(tags)
mode = "auto-detect from tags at HEAD"

unknown = sorted(set(packages) - set(all_packages))
if unknown:
raise ValueError(f"Unknown packages: {', '.join(unknown)}")

return packages, mode
Loading
Loading