From 09fde01b3192f751996a7805b43efe8be5480485 Mon Sep 17 00:00:00 2001 From: David Kirov Date: Wed, 18 Mar 2026 15:17:19 +0100 Subject: [PATCH 1/4] feat(ci): add days-since-last-pin daily workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add .github/workflows/days-since-last-pin.yml that runs daily at 9:42 UTC - Computes days since INTEGRATIONS_CORE_VERSION was last updated in datadog-agent/release.json - Posts gauge metric integrations_core.days_since_last_pin{team:agent-integrations} to Datadog API v2 Rationale: AI-6462 — need a CI dashboard counter that turns red when the agent repo hasn't been pinned in >4 days --- .github/workflows/days-since-last-pin.yml | 111 ++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 .github/workflows/days-since-last-pin.yml diff --git a/.github/workflows/days-since-last-pin.yml b/.github/workflows/days-since-last-pin.yml new file mode 100644 index 0000000000000..af58219356729 --- /dev/null +++ b/.github/workflows/days-since-last-pin.yml @@ -0,0 +1,111 @@ +name: Days Since Last Pin + +on: + schedule: + - cron: "42 9 * * *" + workflow_dispatch: + +jobs: + compute-days-since-last-pin: + runs-on: ubuntu-22.04 + permissions: + contents: read + steps: + - name: Compute and submit days since last pin + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DD_API_KEY: ${{ secrets.DD_API_KEY }} + run: | + python3 - <<'EOF' + import json + import os + import urllib.request + from datetime import datetime, timezone + + AGENT_REPO = "DataDog/datadog-agent" + RELEASE_JSON_URL = f"https://raw.githubusercontent.com/{AGENT_REPO}/main/release.json" + COMMITS_API_URL = f"https://api.github.com/repos/{AGENT_REPO}/commits" + DD_METRICS_URL = "https://api.datadoghq.com/api/v2/series" + METRIC_NAME = "integrations_core.days_since_last_pin" + TAGS = ["team:agent-integrations"] + GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] + DD_API_KEY = os.environ["DD_API_KEY"] + + + def fetch_json(url, headers=None): + req = urllib.request.Request(url, headers=headers or {}) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode()) + + + def get_integrations_core_version(sha): + raw_url = f"https://raw.githubusercontent.com/{AGENT_REPO}/{sha}/release.json" + req = urllib.request.Request(raw_url, headers={"Authorization": f"token {GITHUB_TOKEN}"}) + with urllib.request.urlopen(req) as resp: + release = json.loads(resp.read().decode()) + return release["dependencies"]["INTEGRATIONS_CORE_VERSION"] + + + # Step 1: get current pin + req = urllib.request.Request(RELEASE_JSON_URL, headers={"Authorization": f"token {GITHUB_TOKEN}"}) + with urllib.request.urlopen(req) as resp: + release_data = json.loads(resp.read().decode()) + current_pin = release_data["dependencies"]["INTEGRATIONS_CORE_VERSION"] + print(f"Current pin: {current_pin}") + + # Step 2: fetch recent commits to release.json + commits_url = f"{COMMITS_API_URL}?path=release.json&per_page=30" + commits = fetch_json(commits_url, headers={ + "Authorization": f"token {GITHUB_TOKEN}", + "Accept": "application/vnd.github+json", + }) + print(f"Fetched {len(commits)} commits touching release.json") + + # Step 3: walk commits to find when pin last changed + last_pin_commit = commits[0] # default: most recent commit + for commit in commits: + sha = commit["sha"] + try: + pin_at_sha = get_integrations_core_version(sha) + except Exception as e: + print(f"Warning: could not fetch release.json at {sha}: {e}") + break + if pin_at_sha == current_pin: + last_pin_commit = commit + else: + break # pin changed here — last_pin_commit is correct + + # Step 4: compute days + committed_at_str = last_pin_commit["commit"]["committer"]["date"] + committed_at = datetime.fromisoformat(committed_at_str.replace("Z", "+00:00")) + now_utc = datetime.now(timezone.utc) + days = (now_utc - committed_at).days + print(f"Last pin commit: {last_pin_commit['sha']} at {committed_at_str}") + print(f"Days since last pin: {days}") + + # Step 5: POST gauge metric to Datadog + timestamp = int(now_utc.timestamp()) + payload = { + "series": [ + { + "metric": METRIC_NAME, + "type": 3, + "points": [{"timestamp": timestamp, "value": days}], + "tags": TAGS, + } + ] + } + body = json.dumps(payload).encode() + req = urllib.request.Request( + DD_METRICS_URL, + data=body, + headers={ + "DD-API-KEY": DD_API_KEY, + "Content-Type": "application/json", + }, + method="POST", + ) + with urllib.request.urlopen(req) as resp: + result = json.loads(resp.read().decode()) + print(f"Datadog API response: {result}") + EOF From 09ccfd392f111a4363acb934d24009727696097b Mon Sep 17 00:00:00 2001 From: David Kirov Date: Wed, 18 Mar 2026 17:19:35 +0100 Subject: [PATCH 2/4] refactor(ci): extract inline script and fix error handling in days-since-last-pin - Extract 90-line embedded Python heredoc to .github/workflows/scripts/days_since_last_pin.py - Add actions/checkout step so the workflow can access the script file - Replace silent break-on-error with raise to fail the job on fetch errors - Split single step into compute + submit for step-level failure attribution - Add comment explaining the per_page=30 assumption Rationale: PR review found inline heredoc diverged from repo convention, and a mid-walk fetch failure would silently submit a falsely-healthy metric This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md) --- .github/workflows/days-since-last-pin.yml | 105 +++--------------- .../workflows/scripts/days_since_last_pin.py | 68 ++++++++++++ 2 files changed, 82 insertions(+), 91 deletions(-) create mode 100644 .github/workflows/scripts/days_since_last_pin.py diff --git a/.github/workflows/days-since-last-pin.yml b/.github/workflows/days-since-last-pin.yml index af58219356729..fc527828bb10e 100644 --- a/.github/workflows/days-since-last-pin.yml +++ b/.github/workflows/days-since-last-pin.yml @@ -11,101 +11,24 @@ jobs: permissions: contents: read steps: - - name: Compute and submit days since last pin + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Compute days since last pin + id: compute env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python3 .github/workflows/scripts/days_since_last_pin.py + + - name: Submit metric to Datadog + env: DD_API_KEY: ${{ secrets.DD_API_KEY }} + DAYS: ${{ steps.compute.outputs.days }} run: | python3 - <<'EOF' - import json - import os - import urllib.request - from datetime import datetime, timezone - - AGENT_REPO = "DataDog/datadog-agent" - RELEASE_JSON_URL = f"https://raw.githubusercontent.com/{AGENT_REPO}/main/release.json" - COMMITS_API_URL = f"https://api.github.com/repos/{AGENT_REPO}/commits" - DD_METRICS_URL = "https://api.datadoghq.com/api/v2/series" - METRIC_NAME = "integrations_core.days_since_last_pin" - TAGS = ["team:agent-integrations"] - GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] - DD_API_KEY = os.environ["DD_API_KEY"] - - - def fetch_json(url, headers=None): - req = urllib.request.Request(url, headers=headers or {}) - with urllib.request.urlopen(req) as resp: - return json.loads(resp.read().decode()) - - - def get_integrations_core_version(sha): - raw_url = f"https://raw.githubusercontent.com/{AGENT_REPO}/{sha}/release.json" - req = urllib.request.Request(raw_url, headers={"Authorization": f"token {GITHUB_TOKEN}"}) - with urllib.request.urlopen(req) as resp: - release = json.loads(resp.read().decode()) - return release["dependencies"]["INTEGRATIONS_CORE_VERSION"] - - - # Step 1: get current pin - req = urllib.request.Request(RELEASE_JSON_URL, headers={"Authorization": f"token {GITHUB_TOKEN}"}) - with urllib.request.urlopen(req) as resp: - release_data = json.loads(resp.read().decode()) - current_pin = release_data["dependencies"]["INTEGRATIONS_CORE_VERSION"] - print(f"Current pin: {current_pin}") - - # Step 2: fetch recent commits to release.json - commits_url = f"{COMMITS_API_URL}?path=release.json&per_page=30" - commits = fetch_json(commits_url, headers={ - "Authorization": f"token {GITHUB_TOKEN}", - "Accept": "application/vnd.github+json", - }) - print(f"Fetched {len(commits)} commits touching release.json") - - # Step 3: walk commits to find when pin last changed - last_pin_commit = commits[0] # default: most recent commit - for commit in commits: - sha = commit["sha"] - try: - pin_at_sha = get_integrations_core_version(sha) - except Exception as e: - print(f"Warning: could not fetch release.json at {sha}: {e}") - break - if pin_at_sha == current_pin: - last_pin_commit = commit - else: - break # pin changed here — last_pin_commit is correct - - # Step 4: compute days - committed_at_str = last_pin_commit["commit"]["committer"]["date"] - committed_at = datetime.fromisoformat(committed_at_str.replace("Z", "+00:00")) - now_utc = datetime.now(timezone.utc) - days = (now_utc - committed_at).days - print(f"Last pin commit: {last_pin_commit['sha']} at {committed_at_str}") - print(f"Days since last pin: {days}") - - # Step 5: POST gauge metric to Datadog - timestamp = int(now_utc.timestamp()) - payload = { - "series": [ - { - "metric": METRIC_NAME, - "type": 3, - "points": [{"timestamp": timestamp, "value": days}], - "tags": TAGS, - } - ] - } - body = json.dumps(payload).encode() - req = urllib.request.Request( - DD_METRICS_URL, - data=body, - headers={ - "DD-API-KEY": DD_API_KEY, - "Content-Type": "application/json", - }, - method="POST", - ) + import json, os, time, urllib.request + days = int(os.environ["DAYS"]) + payload = json.dumps({"series": [{"metric": "integrations_core.days_since_last_pin", "type": 3, "points": [{"timestamp": int(time.time()), "value": days}], "tags": ["team:agent-integrations"]}]}).encode() + req = urllib.request.Request("https://api.datadoghq.com/api/v2/series", data=payload, headers={"DD-API-KEY": os.environ["DD_API_KEY"], "Content-Type": "application/json"}, method="POST") with urllib.request.urlopen(req) as resp: - result = json.loads(resp.read().decode()) - print(f"Datadog API response: {result}") + print(f"Datadog API response: {json.loads(resp.read().decode())}") EOF diff --git a/.github/workflows/scripts/days_since_last_pin.py b/.github/workflows/scripts/days_since_last_pin.py new file mode 100644 index 0000000000000..ca64d17d5753a --- /dev/null +++ b/.github/workflows/scripts/days_since_last_pin.py @@ -0,0 +1,68 @@ +import json +import os +import urllib.request +from datetime import datetime, timezone + +AGENT_REPO = "DataDog/datadog-agent" +RELEASE_JSON_URL = f"https://raw.githubusercontent.com/{AGENT_REPO}/main/release.json" +COMMITS_API_URL = f"https://api.github.com/repos/{AGENT_REPO}/commits" +GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] + + +def fetch_json(url, headers=None): + req = urllib.request.Request(url, headers=headers or {}) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode()) + + +def get_integrations_core_version(sha): + raw_url = f"https://raw.githubusercontent.com/{AGENT_REPO}/{sha}/release.json" + req = urllib.request.Request(raw_url, headers={"Authorization": f"token {GITHUB_TOKEN}"}) + with urllib.request.urlopen(req) as resp: + release = json.loads(resp.read().decode()) + return release["dependencies"]["INTEGRATIONS_CORE_VERSION"] + + +# Step 1: get current pin +req = urllib.request.Request(RELEASE_JSON_URL, headers={"Authorization": f"token {GITHUB_TOKEN}"}) +with urllib.request.urlopen(req) as resp: + release_data = json.loads(resp.read().decode()) +current_pin = release_data["dependencies"]["INTEGRATIONS_CORE_VERSION"] +print(f"Current pin: {current_pin}") + +# Step 2: fetch recent commits to release.json +# 30 commits covers many months of bi-weekly pins; pin unchanged across all 30 is extremely unlikely +commits_url = f"{COMMITS_API_URL}?path=release.json&per_page=30" +commits = fetch_json(commits_url, headers={ + "Authorization": f"token {GITHUB_TOKEN}", + "Accept": "application/vnd.github+json", +}) +print(f"Fetched {len(commits)} commits touching release.json") + +# Step 3: walk commits newest→oldest to find when the pin last changed +last_pin_commit = commits[0] +for commit in commits: + sha = commit["sha"] + try: + pin_at_sha = get_integrations_core_version(sha) + except Exception as e: + print(f"Error: could not fetch release.json at {sha}: {e}") + raise # fail the job; don't submit a potentially wrong metric + if pin_at_sha == current_pin: + last_pin_commit = commit + else: + break # pin changed here — last_pin_commit is correct + +# Step 4: compute days +committed_at_str = last_pin_commit["commit"]["committer"]["date"] +committed_at = datetime.fromisoformat(committed_at_str.replace("Z", "+00:00")) +now_utc = datetime.now(timezone.utc) +days = (now_utc - committed_at).days +print(f"Last pin commit: {last_pin_commit['sha']} at {committed_at_str}") +print(f"Days since last pin: {days}") + +# Write days to GITHUB_OUTPUT for the submit step +github_output = os.environ.get("GITHUB_OUTPUT") +if github_output: + with open(github_output, "a") as f: + f.write(f"days={days}\n") From 802231b88fe954de2499894d7b477632f892c8a9 Mon Sep 17 00:00:00 2001 From: David Kirov Date: Wed, 18 Mar 2026 17:41:58 +0100 Subject: [PATCH 3/4] fix(ci): bound commit walk by 30-day window instead of fixed page count - Replace per_page=30 with a since=30-days-ago query parameter and full pagination - release.json is updated for many dep changes (JMXFETCH, OMNIBUS_RUBY, etc.), so a fixed page count could exhaust without finding the pin-change commit - When no commits found in the window, report days=30 (pin is at least that old) Rationale: the sparse-commit assumption was wrong; time-bounded window is the correct approach This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md) --- .../workflows/scripts/days_since_last_pin.py | 70 ++++++++++++------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/.github/workflows/scripts/days_since_last_pin.py b/.github/workflows/scripts/days_since_last_pin.py index ca64d17d5753a..61b189e301117 100644 --- a/.github/workflows/scripts/days_since_last_pin.py +++ b/.github/workflows/scripts/days_since_last_pin.py @@ -1,7 +1,7 @@ import json import os import urllib.request -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone AGENT_REPO = "DataDog/datadog-agent" RELEASE_JSON_URL = f"https://raw.githubusercontent.com/{AGENT_REPO}/main/release.json" @@ -30,36 +30,52 @@ def get_integrations_core_version(sha): current_pin = release_data["dependencies"]["INTEGRATIONS_CORE_VERSION"] print(f"Current pin: {current_pin}") -# Step 2: fetch recent commits to release.json -# 30 commits covers many months of bi-weekly pins; pin unchanged across all 30 is extremely unlikely -commits_url = f"{COMMITS_API_URL}?path=release.json&per_page=30" -commits = fetch_json(commits_url, headers={ - "Authorization": f"token {GITHUB_TOKEN}", - "Accept": "application/vnd.github+json", -}) -print(f"Fetched {len(commits)} commits touching release.json") +# Steps 2 & 3: fetch commits to release.json from the last month, walk newest→oldest. +# release.json is updated for many reasons beyond the integrations-core pin, so we bound by +# time window rather than a fixed page count. If the pin has been unchanged for the full month, +# last_pin_commit will hold the oldest commit in the window (a conservative undercount, but +# still well above the 4-day alert threshold). +since = (datetime.now(timezone.utc) - timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%SZ") +last_pin_commit: dict | None = None +pin_changed = False +page = 1 -# Step 3: walk commits newest→oldest to find when the pin last changed -last_pin_commit = commits[0] -for commit in commits: - sha = commit["sha"] - try: - pin_at_sha = get_integrations_core_version(sha) - except Exception as e: - print(f"Error: could not fetch release.json at {sha}: {e}") - raise # fail the job; don't submit a potentially wrong metric - if pin_at_sha == current_pin: - last_pin_commit = commit - else: - break # pin changed here — last_pin_commit is correct +while not pin_changed: + page_commits = fetch_json( + f"{COMMITS_API_URL}?path=release.json&per_page=100&since={since}&page={page}", + headers={"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github+json"}, + ) + if not page_commits: + break + print(f"Page {page}: fetched {len(page_commits)} commits touching release.json") + if last_pin_commit is None: + last_pin_commit = page_commits[0] # most recent commit, used as fallback + for commit in page_commits: + sha = commit["sha"] + try: + pin_at_sha = get_integrations_core_version(sha) + except Exception as e: + print(f"Error: could not fetch release.json at {sha}: {e}") + raise # fail the job; don't submit a potentially wrong metric + if pin_at_sha == current_pin: + last_pin_commit = commit + else: + pin_changed = True + break # last_pin_commit is the oldest commit still on the current pin + page += 1 # Step 4: compute days -committed_at_str = last_pin_commit["commit"]["committer"]["date"] -committed_at = datetime.fromisoformat(committed_at_str.replace("Z", "+00:00")) now_utc = datetime.now(timezone.utc) -days = (now_utc - committed_at).days -print(f"Last pin commit: {last_pin_commit['sha']} at {committed_at_str}") -print(f"Days since last pin: {days}") +if last_pin_commit is None: + # No commits to release.json in the last 30 days — pin is at least 30 days old + days = 30 + print("No commits to release.json found in the last 30 days; reporting days=30") +else: + committed_at_str = last_pin_commit["commit"]["committer"]["date"] + committed_at = datetime.fromisoformat(committed_at_str.replace("Z", "+00:00")) + days = (now_utc - committed_at).days + print(f"Last pin commit: {last_pin_commit['sha']} at {committed_at_str}") + print(f"Days since last pin: {days}") # Write days to GITHUB_OUTPUT for the submit step github_output = os.environ.get("GITHUB_OUTPUT") From 4a1a7587e5dd202ed9e18fd7b1ea87d9296eadc6 Mon Sep 17 00:00:00 2001 From: David Kirov Date: Thu, 19 Mar 2026 11:25:57 +0100 Subject: [PATCH 4/4] refactor(ci): replace GITHUB_TOKEN with dd-octo-sts for cross-repo access - Add dd-octo-sts-action step to exchange OIDC token for a scoped token on DataDog/datadog-agent (contents:read only) - Pass dd-octo-sts token as GITHUB_TOKEN to the compute step instead of the built-in secrets.GITHUB_TOKEN - Add id-token:write permission to the job for OIDC federation Trust policy PR: DataDog/datadog-agent#48035 Rationale: scoped short-lived token is more secure than using the default GITHUB_TOKEN for cross-repo access This commit made by [/dd:git:commit:quick](https://github.com/DataDog/claude-marketplace/tree/main/dd/commands/git/commit/quick.md) --- .github/workflows/days-since-last-pin.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/days-since-last-pin.yml b/.github/workflows/days-since-last-pin.yml index fc527828bb10e..f12bcd8f5d411 100644 --- a/.github/workflows/days-since-last-pin.yml +++ b/.github/workflows/days-since-last-pin.yml @@ -10,13 +10,20 @@ jobs: runs-on: ubuntu-22.04 permissions: contents: read + id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: DataDog/dd-octo-sts-action@acaa02eee7e3bb0839e4272dacb37b8f3b58ba80 # v1.0.3 + id: octo-sts + with: + scope: DataDog/datadog-agent + policy: integrations-core.github.read-release-json.schedule + - name: Compute days since last pin id: compute env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.octo-sts.outputs.token }} run: python3 .github/workflows/scripts/days_since_last_pin.py - name: Submit metric to Datadog