diff --git a/.github/workflows/example-cross-repo.yml b/.github/workflows/example-cross-repo.yml index 50a8637..1ebc2da 100644 --- a/.github/workflows/example-cross-repo.yml +++ b/.github/workflows/example-cross-repo.yml @@ -23,7 +23,8 @@ jobs: # Use the local version of the action (after you've made the changes) uses: ./ with: - TARGET_REPO: "my-org/target-repo" # Replace with your target repository + TARGET_REPO: "target-repo" # Replace with your target repository name (without org) + OWNER: "my-org" # Replace with your organization name TOKEN: ${{ secrets.CROSS_REPO_TOKEN }} # Token with access to the target repository CLOSE_ISSUES: "true" # Optional: close issues when TODOs are removed AUTO_ASSIGN: "true" # Optional: assign issues to the committer diff --git a/GitHubClient.py b/GitHubClient.py index af10568..5e77b51 100644 --- a/GitHubClient.py +++ b/GitHubClient.py @@ -16,8 +16,25 @@ class GitHubClient(Client): raise EnvironmentError self.base_url = f'{self.github_url}/' self.repos_url = f'{self.base_url}repos/' - self.repo = os.getenv('INPUT_REPO') - self.target_repo = os.getenv('INPUT_TARGET_REPO', self.repo) # Default to current repo if not specified + self.repo = os.getenv('INPUT_REPO') + self.target_repo_name = os.getenv('INPUT_TARGET_REPO') + self.owner = os.getenv('INPUT_OWNER') + + # Construct the full target repository path + if self.target_repo_name and self.owner: + self.target_repo = f'{self.owner}/{self.target_repo_name}' + elif self.target_repo_name and '/' in self.target_repo_name: + # Backward compatibility: if TARGET_REPO contains '/', treat it as full repo path + self.target_repo = self.target_repo_name + print('WARNING: Using full repository path in TARGET_REPO is deprecated. Use OWNER and TARGET_REPO separately.') + else: + self.target_repo = self.repo # Default to current repo if not specified + + # Log the repository configuration for debugging + if self.target_repo == self.repo: + print(f'Creating issues in current repository: {self.repo}') + else: + print(f'Creating issues in target repository: {self.target_repo} (source: {self.repo})') self.before = os.getenv('INPUT_BEFORE') self.sha = os.getenv('INPUT_SHA') self.commits = json.loads(os.getenv('INPUT_COMMITS')) or [] @@ -77,14 +94,15 @@ class GitHubClient(Client): # There is a valid before SHA to compare with, or this is a release being created. diff_url = f'{self.repos_url}{self.repo}/compare/{self.before}...{self.sha}' elif len(self.commits) == 1: - # There is only one commit. - diff_url = f'{self.repos_url}{self.repo}/commits/{self.sha}' + # There is only one commit - compare against empty tree to get all files + diff_url = f'{self.repos_url}{self.repo}/compare/4b825dc642cb6eb9a060e54bf8d69288fbee4904...{self.sha}' elif len(self.commits) > 1: # There are several commits: compare with the oldest one. oldest = sorted(self.commits, key=self._get_timestamp)[0]['id'] diff_url = f'{self.repos_url}{self.repo}/compare/{oldest}...{self.sha}' else: - return None + # No commits info available, compare against empty tree to get all files + diff_url = f'{self.repos_url}{self.repo}/compare/4b825dc642cb6eb9a060e54bf8d69288fbee4904...{self.sha}' diff_headers = { 'Accept': 'application/vnd.github.v3.diff', diff --git a/README.md b/README.md index 6dcf5a7..af6ee7c 100644 --- a/README.md +++ b/README.md @@ -36,107 +36,135 @@ See [Upgrading](#upgrading) for breaking changes. ## Usage -Simply add a line or block comment starting with TODO (or any other comment identifiers configured), followed by a colon and/or space. +### Basic Usage (Same Repository) -Here's an example for Python creating an issue named after the TODO _description_: +For most users, you can use the action without any additional configuration. Issues will be created in the same repository where your code and TODOs exist: -```python -def hello_world(): - # TODO: Come up with a more imaginative greeting - print('Hello world!') +```yaml +name: "TODO to Issue" +on: ["push"] +jobs: + build: + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Ensures all commits are available for processing existing TODOs + - name: "TODO to Issue" + uses: "alstr/todo-to-issue-action@v5" ``` -_Multiline_ TODOs are supported, with additional lines inserted into the issue body: +### Cross-Repository Usage (Advanced) -```python -def hello_world(): - # TODO: Come up with a more imaginative greeting - # Everyone uses hello world and it's boring. - print('Hello world!') +If you want to create issues in a different repository (e.g., a GitOps repository), you'll need to set up a GitHub App and provide additional configuration: + +```yaml +name: "TODO to Issue" +on: ["push"] +jobs: + build: + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: "TODO to Issue" + uses: "alstr/todo-to-issue-action@v5" + with: + TARGET_REPO: "gitops-repo" + APP_ID: ${{ secrets.APP_ID }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + OWNER: "your-org" ``` -### Cross-Repository Issues +## Configuration -The action supports creating issues in a different repository than the one where the TODO comments are found. This requires: +### Required for Cross-Repository Usage -1. Setting the `TARGET_REPO` input to specify the target repository (e.g., "owner/repo") +| Input | Description | Required | +| ------------- | --------------------------------------------------------------- | ------------------- | +| `TARGET_REPO` | Target repository name to create issues in (e.g. 'gitops-repo') | Only for cross-repo | +| `APP_ID` | GitHub App ID for generating tokens | Only for cross-repo | +| `PRIVATE_KEY` | Private key for the GitHub App | Only for cross-repo | +| `OWNER` | Owner of the target repository (e.g. 'your-org') | Only for cross-repo | -2. Providing appropriate authentication: - - For public repositories or when the target repository is in the same organization, use the default `GITHUB_TOKEN`: - ```yaml - - uses: alstr/todo-to-issue-action@v5 - with: - TARGET_REPO: "my-org/target-repo" - ``` - - For private repositories, provide GitHub App credentials: - ```yaml - - uses: alstr/todo-to-issue-action@v5 - with: - TARGET_REPO: "target-org/target-repo" - APP_ID: ${{ secrets.APP_ID }} - PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} - OWNER: "target-org" - ``` +### Optional Inputs -The action will automatically: -1. Use the default `GITHUB_TOKEN` for same-repo or public repository access -2. Generate a GitHub App token when `APP_ID` and `PRIVATE_KEY` are provided for private repository access +| Input | Description | Default | +| ------------------- | ------------------------------------ | --------- | +| `CLOSE_ISSUES` | Close issues when TODOs are removed | `"true"` | +| `AUTO_P` | Format multiline TODOs as paragraphs | `"true"` | +| `AUTO_ASSIGN` | Auto-assign issues to the actor | `"false"` | +| `ESCAPE` | Escape Markdown characters | `"true"` | +| `INSERT_ISSUE_URLS` | Insert issue URLs into TODO comments | `"false"` | -### GitHub App Setup +## GitHub App Setup (For Cross-Repository Usage) -To use the action with private repositories, you'll need to: +If you want to create issues in a different repository, you'll need to create a GitHub App: -1. Create a GitHub App: - - Go to your GitHub account settings - - Navigate to "Developer settings" (bottom of the left sidebar) - - Click "GitHub Apps" and then "New GitHub App" - - Fill in the following details: - - **GitHub App name**: Choose a unique name (e.g., "TODO-to-Issue-App") - - **Homepage URL**: Your repository URL - - **Webhook**: Leave disabled - - **Repository permissions**: - - Issues: Read & Write - - Pull requests: Read & Write (if using PR integration) - - **Where can this GitHub App be installed?**: Any account - - Click "Create GitHub App" - - On the next page, click "Generate a private key" to download your private key file - - Note down the "App ID" shown on the page +### 1. Create a GitHub App -2. Store the following secrets in your repository: - - Go to your repository's "Settings" → "Secrets and variables" → "Actions" - - Add two new repository secrets: - - `APP_ID`: The App ID you noted down - - `PRIVATE_KEY`: The entire contents of the private key file you downloaded (including the BEGIN and END lines) +1. Go to your GitHub organization/user settings +2. Navigate to "Developer settings" > "GitHub Apps" +3. Click "New GitHub App" +4. Fill in the required fields: + - **App name**: Choose a unique name + - **Homepage URL**: Your repository or organization URL + - **Webhook URL**: Can be a placeholder like `https://example.com` -3. Install the GitHub App: - - Go back to your GitHub App settings - - Click "Install App" in the left sidebar - - Choose the target repository where you want to create issues - - Click "Install" +### 2. Set Permissions -As per the [Google Style Guide](https://google.github.io/styleguide/cppguide.html#TODO_Comments), you can provide a _reference_ after the TODO identifier: +Grant the following **Repository permissions**: +- **Contents**: Read (to access source code and diffs) +- **Issues**: Write (to create and manage issues) +- **Metadata**: Read (required) +- **Pull requests**: Read (if using with PRs) -```python -def hello_world(): - # TODO(@alstr): Come up with a more imaginative greeting - # This will assign the issue to alstr. - print('Hello world!') +### 3. Generate and Store Secrets - # TODO(!urgent): This is wrong - # This will add an 'urgent' label. - assert 1 + 1 == 3 +1. **Private Key**: Generate and download the private key +2. **App ID**: Note the App ID from the app settings +3. Store both in your repository secrets: + - `APP_ID`: The numeric App ID + - `PRIVATE_KEY`: The entire private key file content - # TODO(#99): We need error handling here - # This will add the comment to the existing issue 99. - greeting_time = datetime.fromisoformat(date_string) +### 4. Install the App - # TODO(language): Localise this string - # This will prepend the reference to the issue title - dialogue = "TODO or not TODO, that is the question." +Install the GitHub App on both: +- The source repository (where your code/TODOs are) +- The target repository (where issues will be created) + +## How It Works + +### First Run +When you first add this action to a repository with existing TODOs: +- With `fetch-depth: 0`, the action has access to full git history +- It compares against an empty tree to treat all files as "new" +- **All existing TODOs** in your codebase will be converted to issues + +### Subsequent Runs +- Only processes TODOs that were added, modified, or removed in the current push +- Updates existing issues when TODOs are modified +- Closes issues when TODOs are removed (if `CLOSE_ISSUES` is enabled) + +## Examples + +### Same Repository (Simple) +```yaml +- uses: "alstr/todo-to-issue-action@v5" ``` -Only one reference can be provided. Should you wish to further configure the issue, you can do so via -[TODO Options](#todo-options). +### Cross Repository with Custom Settings +```yaml +- uses: "alstr/todo-to-issue-action@v5" + with: + TARGET_REPO: "issues-repo" + APP_ID: ${{ secrets.TODO_APP_ID }} + PRIVATE_KEY: ${{ secrets.TODO_PRIVATE_KEY }} + OWNER: "myorg" + AUTO_ASSIGN: "true" + CLOSE_ISSUES: "false" +``` ## TODO Options diff --git a/action.yml b/action.yml index bb7ee45..81ed21b 100644 --- a/action.yml +++ b/action.yml @@ -5,18 +5,19 @@ runs: using: "composite" steps: - name: Extract repository names - if: ${{ inputs.TARGET_REPO != '' && inputs.APP_ID != '' }} + if: ${{ inputs.TARGET_REPO != '' && inputs.OWNER != '' && inputs.APP_ID != '' }} id: extract-repos run: | SOURCE_REPO_NAME=$(echo "${{ inputs.REPO }}" | cut -d'/' -f2) - TARGET_REPO_NAME=$(echo "${{ inputs.TARGET_REPO }}" | cut -d'/' -f2) + # TARGET_REPO now only contains the repo name, not the full path + TARGET_REPO_NAME="${{ inputs.TARGET_REPO }}" echo "source-repo-name=$SOURCE_REPO_NAME" >> $GITHUB_OUTPUT echo "target-repo-name=$TARGET_REPO_NAME" >> $GITHUB_OUTPUT echo "repositories=$SOURCE_REPO_NAME,$TARGET_REPO_NAME" >> $GITHUB_OUTPUT shell: bash - name: Generate token for private repository access - if: ${{ inputs.TARGET_REPO != '' && inputs.APP_ID != '' }} + if: ${{ inputs.TARGET_REPO != '' && inputs.OWNER != '' && inputs.APP_ID != '' }} id: generate-token uses: actions/create-github-app-token@v2 with: @@ -80,7 +81,7 @@ inputs: required: false default: ${{ github.token }} TARGET_REPO: - description: "Optional target repository to create issues in (e.g. 'owner/repo'). If not provided, issues will be created in the current repository." + description: "Optional target repository name to create issues in (e.g. 'gitops-repo'). Use with OWNER to specify the full repository. If not provided, issues will be created in the current repository." required: false APP_ID: description: "GitHub App ID for generating tokens to access private repositories" @@ -89,16 +90,16 @@ inputs: description: "Private key for the GitHub App (do not enter the actual secret)" required: false OWNER: - description: "Owner of the target repository (required if using GitHub App authentication)" + description: "Owner of the target repository (e.g. 'your-org'). Required when using TARGET_REPO or GitHub App authentication." required: false CLOSE_ISSUES: description: "Optional input specifying whether to attempt to close an issue when a TODO is removed" required: false - default: true + default: "true" AUTO_P: description: "For multiline TODOs, format each line as a new paragraph when creating the issue" required: false - default: true + default: "true" PROJECT: description: "User or organization project to link issues to, format 'project_type/owner/project_name'" required: false @@ -111,7 +112,7 @@ inputs: AUTO_ASSIGN: description: "Automatically assign new issues to the user who triggered the action" required: false - default: false + default: "false" ACTOR: description: "The username of the person who triggered the action (automatically set)" required: false @@ -133,15 +134,15 @@ inputs: ESCAPE: description: "Escape all special Markdown characters" required: false - default: true + default: "true" LANGUAGES: description: "A collection of comma-delimited URLs or local paths for custom language files" required: false NO_STANDARD: description: "Exclude loading the default 'syntax.json' and 'languages.yml' files from the repository" required: false - default: false + default: "false" INSERT_ISSUE_URLS: description: "Whether the action should insert the URL for a newly-created issue into the associated TODO comment" required: false - default: false + default: "false"