From ca95281faa3a1e60ec0cda6478e0fb841e54fdb7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:49:57 +0000 Subject: [PATCH 1/3] Initial plan From da84be0b793cca357316a3a687077d93bf0fa3bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:54:38 +0000 Subject: [PATCH 2/3] ci: add GitHub Actions workflows from api-simulator repo Agent-Logs-Url: https://github.com/counterfact/example-petstore/sessions/91ad185b-cfc6-4500-9f71-4afb09eb4f68 Co-authored-by: pmcelhaney <51504+pmcelhaney@users.noreply.github.com> --- .github/scripts/create_issues.py | 300 ++++++++++++++++++++++++++++ .github/workflows/ci.yaml | 46 +++++ .github/workflows/codeql.yml | 77 +++++++ .github/workflows/create-issues.yml | 44 ++++ 4 files changed, 467 insertions(+) create mode 100644 .github/scripts/create_issues.py create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/create-issues.yml diff --git a/.github/scripts/create_issues.py b/.github/scripts/create_issues.py new file mode 100644 index 0000000..62b4f74 --- /dev/null +++ b/.github/scripts/create_issues.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +"""Process issue proposal files and create GitHub issues. + +Reads every *.md file under .github/issue-proposals/, parses the YAML front +matter, validates required fields, creates a GitHub issue via the gh CLI, links +it as a sub-issue of the resolved parent, then deletes the proposal file. + +Exit codes: + 0 – all proposals processed successfully (or no proposals found) + 1 – one or more proposals failed validation or issue creation +""" + +import glob +import os +import re +import subprocess +import sys + +import yaml + + +def parse_proposal(filepath): + """Return (metadata dict, body str) for a Markdown proposal file. + + Raises ValueError if the file is malformed. + """ + with open(filepath, encoding="utf-8") as fh: + content = fh.read() + + if not content.startswith("---"): + raise ValueError(f"No YAML front matter found in {filepath!r}") + + parts = content.split("---", 2) + if len(parts) < 3: + raise ValueError(f"Front matter is not properly closed in {filepath!r}") + + raw_yaml = parts[1] + body = parts[2].strip() + + try: + metadata = yaml.safe_load(raw_yaml) + except yaml.YAMLError as exc: + raise ValueError(f"Invalid YAML in front matter of {filepath!r}: {exc}") from exc + + if not isinstance(metadata, dict): + raise ValueError(f"Front matter must be a YAML mapping in {filepath!r}") + + return metadata, body + + +def _gh(*args, check=True): + """Run a gh CLI command and return its stdout, raising on failure.""" + result = subprocess.run( + ["gh", *args], + capture_output=True, + text=True, + ) + if check and result.returncode != 0: + raise RuntimeError(result.stderr.strip() or result.stdout.strip()) + return result + + +def get_parent_issue_from_context(repo, sha): + """Try to derive a parent issue number from the pull request context. + + Returns an int or None. + """ + # Find the PR associated with this commit. + result = _gh( + "api", + f"repos/{repo}/commits/{sha}/pulls", + "--jq", + ".[0].number", + check=False, + ) + if result.returncode != 0 or not result.stdout.strip(): + return None + + pr_number = result.stdout.strip() + + # Get the head branch of that PR. + result = _gh( + "api", + f"repos/{repo}/pulls/{pr_number}", + "--jq", + ".head.ref", + check=False, + ) + if result.returncode != 0 or not result.stdout.strip(): + return None + + branch = result.stdout.strip() + + # Extract an issue number from the branch name. + # Try common naming conventions in order of specificity before falling back + # to any bare number, so that version numbers in prefixes (e.g. "v2-") are + # not mistakenly treated as issue numbers. + # + # Supported patterns (examples): + # copilot/fix-1234-description → issue-/fix-/feat-/bug- prefix + # feature/issue-1234 → "issue-" keyword + # 1234-my-feature → leading number in the last path segment + # my-feature-1234 → trailing number in the last path segment + + segment = branch.split("/")[-1] # consider only the last path component + + for pattern in ( + r"(?:issue|fix|feat|feature|bug|chore|refactor)[-/](\d+)", # keyword[-/]number (e.g. "issue-1234", "fix/1234") + r"^(\d+)[-_]", # leading number in the segment (e.g. "1234-description") + r"[-_](\d+)$", # trailing number in the segment (e.g. "description-1234") + r"[-_](\d+)[-_]", # number surrounded by separators + ): + match = re.search(pattern, segment) + if match: + return int(match.group(1)) + + return None + + +def resolve_parent_issue(metadata, repo, sha): + """Return the parent issue number as int, or None.""" + # Front matter takes precedence when present and valid. + raw = metadata.get("parentIssue") + if raw is not None: + try: + return int(raw) + except (ValueError, TypeError): + pass + + return get_parent_issue_from_context(repo, sha) + + +def create_github_issue(repo, title, body, labels, assignees, milestone): + """Create a GitHub issue via the GitHub API. Return (issue_number, issue_id, issue_url). + + Uses ``gh api`` so the JSON response can be parsed reliably, avoiding any + dependency on the URL format printed by ``gh issue create``. + + Returns a 3-tuple: (issue_number, issue_id, issue_url). + ``issue_id`` is the internal integer ID required by the sub-issues API. + ``issue_number`` is the human-readable number shown in the URL. + """ + fields = ["-f", f"title={title}", "-f", f"body={body}"] + + for label in labels: + fields += ["-f", f"labels[]={label}"] + + for assignee in assignees: + fields += ["-f", f"assignees[]={assignee}"] + + if milestone: + fields += ["-f", f"milestone={milestone}"] + + result = _gh( + "api", + "--method", + "POST", + "-H", + "Accept: application/vnd.github+json", + f"repos/{repo}/issues", + *fields, + "--jq", + ".number,.id,.html_url", + ) + + lines = result.stdout.strip().splitlines() + if len(lines) < 3: + raise RuntimeError(f"Unexpected response from issues API: {result.stdout!r}") + + issue_number = int(lines[0]) + issue_id = int(lines[1]) + issue_url = lines[2] + return issue_number, issue_id, issue_url + + +def add_sub_issue(repo, parent_number, child_issue_id): + """Register the issue identified by child_issue_id as a sub-issue of parent_number. + + Uses the GitHub sub-issues REST API endpoint: + POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues + + ``child_issue_id`` must be the internal integer ID of the issue (the ``.id`` + field in the GitHub API response), **not** the human-readable issue number. + See https://docs.github.com/en/rest/issues/sub-issues + + This endpoint requires the repository to have the Sub-issues feature + enabled (available on GitHub Team and Enterprise plans, and as a public + beta on some Free plans). If the endpoint is unavailable, a warning is + logged and the function returns False without failing the overall workflow. + """ + result = _gh( + "api", + "--method", + "POST", + "-H", + "Accept: application/vnd.github+json", + f"repos/{repo}/issues/{parent_number}/sub_issues", + "-f", + f"sub_issue_id={child_issue_id}", + check=False, + ) + if result.returncode != 0: + print( + f" Warning: could not create sub-issue relationship: {result.stderr.strip()}", + file=sys.stderr, + ) + return False + return True + + +def main(): + repo = os.environ.get("GITHUB_REPOSITORY", "") + sha = os.environ.get("GITHUB_SHA", "") + + if not repo: + print("Error: GITHUB_REPOSITORY environment variable is not set.", file=sys.stderr) + sys.exit(1) + + proposal_dir = ".github/issue-proposals" + files = sorted(glob.glob(f"{proposal_dir}/*.md")) + + if not files: + print("No proposal files found — nothing to do.") + return + + failed = False + files_to_delete = [] + + for filepath in files: + print(f"\nProcessing: {filepath}") + + # --- Parse --- + try: + metadata, body = parse_proposal(filepath) + except ValueError as exc: + print(f" Error: {exc}", file=sys.stderr) + failed = True + continue + + # --- Validate --- + title = (metadata.get("title") or "").strip() + if not title: + print(f" Error: 'title' is missing or empty in {filepath!r}", file=sys.stderr) + failed = True + continue + + if not body: + print(f" Error: body content is empty in {filepath!r}", file=sys.stderr) + failed = True + continue + + # --- Determine parent --- + parent_issue = resolve_parent_issue(metadata, repo, sha) + if parent_issue is None: + print( + f" Error: cannot determine parent issue for {filepath!r}. " + "Add 'parentIssue' to front matter or ensure the PR branch " + "includes an issue number.", + file=sys.stderr, + ) + failed = True + continue + + print(f" Title: {title}") + print(f" Parent issue: #{parent_issue}") + + # --- Create issue --- + labels = [str(lbl) for lbl in (metadata.get("labels") or [])] + assignees = [str(a) for a in (metadata.get("assignees") or [])] + milestone = metadata.get("milestone") + + try: + issue_number, issue_id, issue_url = create_github_issue( + repo, title, body, labels, assignees, milestone + ) + except RuntimeError as exc: + print(f" Error creating issue: {exc}", file=sys.stderr) + failed = True + continue + + print(f" Created: #{issue_number} — {issue_url}") + + # --- Sub-issue link --- + linked = add_sub_issue(repo, parent_issue, issue_id) + if linked: + print(f" Linked as sub-issue of #{parent_issue}") + + files_to_delete.append(filepath) + + # --- Cleanup --- + for filepath in files_to_delete: + os.remove(filepath) + print(f"\nDeleted: {filepath}") + + if failed: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..a011636 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,46 @@ +name: CI Checks + +on: + pull_request: + branches: + - main + merge_group: + types: [checks_requested] + workflow_dispatch: + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + ci-checks: + name: CI Checks + permissions: + contents: read + strategy: + matrix: + os: [windows-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Debug + run: echo "${{ matrix.os }}" + - name: Check Out Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: npm + id: setup-node + - name: Get Node Version + run: echo "version=$(node -v)" >> $GITHUB_OUTPUT + id: node-version + shell: bash + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: node_modules + key: ${{ runner.os }}-${{ steps.node-version.outputs.version }}-${{ hashFiles('**/package-lock.json') }} + - name: Install Packages + run: npm ci + - name: Test + run: npm test diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..74d0227 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,77 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: CodeQL + +on: + push: + branches: [main] + pull_request: + # The branches below must be a subset of the branches above + branches: [main] + schedule: + - cron: "43 18 * * 1" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [javascript] + + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/create-issues.yml b/.github/workflows/create-issues.yml new file mode 100644 index 0000000..29448ed --- /dev/null +++ b/.github/workflows/create-issues.yml @@ -0,0 +1,44 @@ +name: Create Issues from Proposals + +on: + push: + branches: + - main + paths: + - '.github/issue-proposals/*.md' + +jobs: + create-issues: + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} + + - name: Install Python dependencies + run: pip install pyyaml + + - name: Process issue proposals + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SHA: ${{ github.sha }} + run: python3 .github/scripts/create_issues.py + + - name: Commit deleted proposal files + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add .github/issue-proposals/ + if git diff --cached --quiet; then + echo "No proposal files were deleted — nothing to commit." + else + git commit -m "chore: remove processed issue proposals" + git pull --rebase + git push + fi From abae66a3aebbc18046d028b7e261efbad8d8ef61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:03:01 +0000 Subject: [PATCH 3/3] fix: add npm test script that exits successfully Agent-Logs-Url: https://github.com/counterfact/example-petstore/sessions/fd3d00ae-8f9b-4a79-a51b-347b00f00f6c Co-authored-by: pmcelhaney <51504+pmcelhaney@users.noreply.github.com> --- package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index dfcbbf5..b3d679d 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,12 @@ "description": "An implementation of the OpenAPI Petstore using Counterfact", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"No tests yet\"", + "start": "counterfact --spec ./spec/petstore.yaml api" }, "author": "Patrick McElhaney", "license": "ISC", "dependencies": { "counterfact": "^2.6.0" - }, - "scripts": { - "start": "counterfact --spec ./spec/petstore.yaml api" } }