-
Notifications
You must be signed in to change notification settings - Fork 3
ci: add GitLab pipeline to bump rshell in datadog-agent on new tag #188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 21 commits
4b8bddb
513701b
b1ad597
2acb6bf
e7ee0eb
a5d0181
d63092d
26f0d98
9e0ed9f
bd57580
20d3ba3
9cf0d68
1b0b333
0633905
3c0ac38
5ecc8eb
5f4aebd
b363e85
b15a36f
9ae59ee
47b1847
810ac25
ef39f15
8537f5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| name: CI scripts | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'tools/**' | ||
| - '.github/workflows/test-ci-scripts.yml' | ||
| pull_request: | ||
| branches: [main] | ||
| paths: | ||
| - 'tools/**' | ||
| - '.github/workflows/test-ci-scripts.yml' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test-bump-datadog-agent: | ||
| name: Test bump_datadog_agent | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| - name: Run unit tests | ||
| run: python3 -m unittest discover -s tools/bump_datadog_agent/tests -v | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| --- | ||
| stages: | ||
| - trigger_release | ||
|
|
||
| .dd_octo_sts: | ||
| id_tokens: | ||
| DDOCTOSTS_ID_TOKEN: | ||
| aud: dd-octo-sts | ||
|
|
||
| .setup_github_bump_token: | ||
| - export GITHUB_TOKEN=$(dd-octo-sts token --scope DataDog/datadog-agent --policy self.rshell.bump-rshell-version) | ||
|
|
||
| bump_datadog_agent: | ||
| stage: trigger_release | ||
| image: registry.ddbuild.io/ci/datadog-agent-buildimages/linux:latest | ||
| tags: ["arch:arm64"] | ||
| extends: .dd_octo_sts | ||
| resource_group: rshell-bump | ||
| rules: | ||
| - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ | ||
| - if: $CI_PIPELINE_SOURCE == "web" && $CI_COMMIT_BRANCH == "main" && $BUMP_VERSION =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ | ||
| before_script: | ||
| # Install dependencies BEFORE minting the GITHUB_TOKEN so a compromised | ||
| # package's install-time hook can't read the token from the environment. | ||
| # Pin to a vetted PyGithub version; review periodically and bump deliberately. | ||
| - pip install "PyGithub==2.5.0" | ||
| - !reference [.setup_github_bump_token] | ||
|
matt-dz marked this conversation as resolved.
Outdated
|
||
| script: | ||
| - python3 tools/bump_datadog_agent/bump.py "${CI_COMMIT_TAG:-$BUMP_VERSION}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| #!/usr/bin/env python3 | ||
| """Open a PR on DataDog/datadog-agent that bumps the pinned rshell version. | ||
|
|
||
| Invoked by the `bump_datadog_agent` GitLab CI job after a new rshell tag is | ||
| detected. Expects: | ||
| - sys.argv[1]: the rshell tag (e.g. "v0.0.11") | ||
| - env GITHUB_TOKEN: a short-lived dd-octo-sts token scoped to | ||
| DataDog/datadog-agent with contents:write + pull-requests:write. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import hashlib | ||
| import os | ||
| import re | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| from pathlib import Path | ||
|
|
||
| TARGET_REPO = "DataDog/datadog-agent" | ||
| TARGET_BASE = "main" | ||
| RSHELL_MODULE = "github.com/DataDog/rshell" | ||
| REVIEW_TEAM = "action-platform" | ||
| PR_LABELS = ["changelog/no-changelog", "ask-review"] | ||
| GIT_USER_NAME = "github-actions[bot]" | ||
| GIT_USER_EMAIL = "github-actions[bot]@users.noreply.github.com" | ||
|
|
||
|
|
||
| def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]: | ||
| return subprocess.run(cmd, cwd=cwd, check=check, text=True, capture_output=True) | ||
|
matt-dz marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def log(msg: str) -> None: | ||
| """Emit a progress line to stdout. Call sites must never pass secrets.""" | ||
| print(f"[bump] {msg}", flush=True) | ||
|
|
||
|
|
||
| def configure_credentials(workdir: Path, token: str) -> Path: | ||
| """Store the GitHub token in a local git credentials file under .git/. | ||
|
|
||
| Keeps the token out of process argv (visible to `ps`), command-line logs, | ||
| and subprocess exception tracebacks. The file is mode 0600 and lives inside | ||
| the ephemeral clone directory, which is discarded when the runner exits. | ||
| """ | ||
| creds_path = workdir / ".git" / "ci-credentials" | ||
| creds_path.write_text(f"https://x-access-token:{token}@github.com\n") | ||
| creds_path.chmod(0o600) | ||
| run(["git", "config", "credential.helper", f"store --file={creds_path}"], cwd=workdir) | ||
| return creds_path | ||
|
|
||
|
|
||
| _RSHELL_REPLACE_RE = re.compile( | ||
| rf"^[ \t]*(?:replace\s+)?{re.escape(RSHELL_MODULE)}(?:\s+v\S+)?\s+=>\s+[^\n]*$\n?", | ||
| re.MULTILINE, | ||
| ) | ||
|
|
||
|
|
||
| def strip_rshell_replace(go_mod: Path) -> None: | ||
| """Remove rshell replace directives from go.mod in every valid Go form. | ||
|
|
||
| Handles: | ||
| - single-line unversioned: replace github.com/DataDog/rshell => /path | ||
| - single-line versioned: replace github.com/DataDog/rshell v0.0.10 => /path | ||
| - block-form entries (no leading `replace` keyword on the line itself) | ||
|
|
||
| Any leftover empty `replace ( )` block is normalized away by `dda inv tidy` | ||
| downstream. | ||
| """ | ||
| original = go_mod.read_text() | ||
| updated = _RSHELL_REPLACE_RE.sub("", original) | ||
| if updated != original: | ||
| go_mod.write_text(updated) | ||
| log(f"stripped replace directive(s) for {RSHELL_MODULE} from go.mod") | ||
|
|
||
|
|
||
| def current_rshell_version(go_mod: Path) -> str | None: | ||
| """Return the rshell version pinned in a `require` declaration, ignoring `replace` lines.""" | ||
| pattern = re.compile( | ||
| rf"^\s*(?:require\s+)?{re.escape(RSHELL_MODULE)}\s+(v\S+)(?!\s*=>)", | ||
| re.MULTILINE, | ||
| ) | ||
| m = pattern.search(go_mod.read_text()) | ||
| return m.group(1) if m else None | ||
|
|
||
|
|
||
| def write_release_note(repo_root: Path, version: str) -> Path: | ||
| # Deterministic per (module, version) so retries produce the identical file. | ||
| suffix = hashlib.sha256(f"{RSHELL_MODULE}@{version}".encode()).hexdigest()[:16] | ||
| note = repo_root / "releasenotes" / "notes" / f"bump-rshell-{version}-{suffix}.yaml" | ||
| note.write_text( | ||
| "---\n" | ||
| "enhancements:\n" | ||
| " - |\n" | ||
| f" Bump ``rshell`` to {version} for the Private Action Runner.\n" | ||
| ) | ||
| return note | ||
|
|
||
|
|
||
| def main() -> int: | ||
| if len(sys.argv) != 2: | ||
| print("usage: bump.py <tag>", file=sys.stderr) | ||
| return 2 | ||
| version = sys.argv[1] | ||
| if not re.fullmatch(r"v\d+\.\d+\.\d+", version): | ||
| print(f"invalid version {version!r}; expected vX.Y.Z", file=sys.stderr) | ||
| return 2 | ||
|
|
||
| token = os.environ.get("GITHUB_TOKEN") | ||
| if not token: | ||
| print("GITHUB_TOKEN is not set; dd-octo-sts exchange failed upstream", file=sys.stderr) | ||
| return 1 | ||
|
|
||
| log(f"preparing bump of {RSHELL_MODULE} to {version}") | ||
| from github import Auth, Github, GithubException | ||
|
|
||
| gh = Github(auth=Auth.Token(token), per_page=100) | ||
| repo = gh.get_repo(TARGET_REPO) | ||
| branch = f"bump-rshell-{version}" | ||
|
|
||
| log(f"checking {TARGET_REPO} for existing PR with head={branch}") | ||
| existing = list(repo.get_pulls(state="open", head=f"{TARGET_REPO.split('/')[0]}:{branch}")) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The idempotency check only looks for open PRs, so if a prior bump PR for the same Useful? React with 👍 / 👎. |
||
| if existing: | ||
| log(f"PR already exists: {existing[0].html_url}; nothing to do") | ||
| return 0 | ||
|
|
||
| # Use a fresh, unique tempdir each run. Auto-cleanup on exit means no stale | ||
| # state leaks between runs; starting empty means `git clone` never fails | ||
| # with exit 128 because a prior directory already existed. | ||
| with tempfile.TemporaryDirectory(prefix="bump-datadog-agent-") as td: | ||
| workdir = Path(td) / "datadog-agent" | ||
| clone_url = f"https://github.com/{TARGET_REPO}.git" | ||
| log(f"cloning {TARGET_REPO}@{TARGET_BASE} into {workdir}") | ||
| run(["git", "clone", "--depth=1", "--branch", TARGET_BASE, clone_url, str(workdir)]) | ||
| log("configuring git credentials (token stored in .git/ci-credentials, not argv)") | ||
| configure_credentials(workdir, token) | ||
| run(["git", "config", "user.name", GIT_USER_NAME], cwd=workdir) | ||
| run(["git", "config", "user.email", GIT_USER_EMAIL], cwd=workdir) | ||
| log(f"creating branch {branch}") | ||
| run(["git", "checkout", "-b", branch], cwd=workdir) | ||
|
|
||
| go_mod = workdir / "go.mod" | ||
| previous_version = current_rshell_version(go_mod) | ||
| log(f"current pinned version in go.mod: {previous_version or '<none>'}") | ||
|
|
||
| if previous_version == version: | ||
| log(f"datadog-agent already pins rshell at {version}; nothing to do") | ||
| return 0 | ||
|
|
||
| strip_rshell_replace(go_mod) | ||
| log(f"running: go get {RSHELL_MODULE}@{version}") | ||
| run(["go", "get", f"{RSHELL_MODULE}@{version}"], cwd=workdir) | ||
| log("running: dda inv tidy") | ||
| run(["dda", "inv", "tidy"], cwd=workdir) | ||
|
|
||
| run(["git", "add", "-A"], cwd=workdir) | ||
| diff = subprocess.run(["git", "diff", "--cached", "--quiet"], cwd=workdir) | ||
| if diff.returncode == 0: | ||
| log(f"no changes to go.mod/go.sum; datadog-agent already at rshell {version}") | ||
| return 0 | ||
|
|
||
| note = write_release_note(workdir, version) | ||
| log(f"wrote release note: {note.relative_to(workdir)}") | ||
| run(["git", "add", str(note)], cwd=workdir) | ||
|
|
||
| commit_msg = ( | ||
| f"Bump rshell dependency from {previous_version} to {version}" | ||
| if previous_version | ||
| else f"Bump rshell dependency to {version}" | ||
| ) | ||
| log(f"committing: {commit_msg}") | ||
| run(["git", "commit", "-m", commit_msg], cwd=workdir) | ||
| log(f"pushing branch {branch} to origin (force)") | ||
| # Force push is safe: this branch is only ever written by this script, and | ||
| # the force handles retries after a prior failure (deterministic tree, | ||
| # non-deterministic commit timestamp). | ||
| run(["git", "push", "--force", "origin", branch], cwd=workdir) | ||
|
|
||
| log("opening draft PR") | ||
| pr = repo.create_pull( | ||
| title=f"[automated] Bump rshell to {version}", | ||
| body=( | ||
| f"Automated bump of `{RSHELL_MODULE}` to " | ||
| f"[{version}](https://github.com/DataDog/rshell/releases/tag/{version}).\n" | ||
| ), | ||
| base=TARGET_BASE, | ||
| head=branch, | ||
| draft=True, | ||
| ) | ||
| log(f"opened draft PR: {pr.html_url}") | ||
|
|
||
| try: | ||
| pr.add_to_labels(*PR_LABELS) | ||
| log(f"added labels: {', '.join(PR_LABELS)}") | ||
| except GithubException as e: | ||
| log(f"warning: failed to add labels {PR_LABELS}: {e}") | ||
|
|
||
| try: | ||
| pr.create_review_request(team_reviewers=[REVIEW_TEAM]) | ||
| log(f"requested review from @DataDog/{REVIEW_TEAM}") | ||
| except GithubException as e: | ||
| log(f"warning: failed to request review from @DataDog/{REVIEW_TEAM}: {e}") | ||
|
|
||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matt-dz
Q: Don't we already have existing tooling that will do this?
Example PRs like this: #198
By "🤖 Generated by DataDog Automated Dependency Management System"