Refactor action.yml and GitHubClient.py to enhance cross-repository issue creation by separating OWNER and TARGET_REPO inputs, improving clarity in usage and documentation. Update README.md to reflect new configuration requirements and provide clearer examples for users. Adjust example workflow to align with new input structure.

Signed-off-by: Shakar Bakr <5h4k4r.b4kr@gmail.com>
This commit is contained in:
Shakar Bakr 2025-06-15 13:28:46 +03:00
parent 9d1f771660
commit 783f67f10b
No known key found for this signature in database
GPG Key ID: DA55A26823AE3C28
4 changed files with 143 additions and 95 deletions

View File

@ -23,7 +23,8 @@ jobs:
# Use the local version of the action (after you've made the changes) # Use the local version of the action (after you've made the changes)
uses: ./ uses: ./
with: 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 TOKEN: ${{ secrets.CROSS_REPO_TOKEN }} # Token with access to the target repository
CLOSE_ISSUES: "true" # Optional: close issues when TODOs are removed CLOSE_ISSUES: "true" # Optional: close issues when TODOs are removed
AUTO_ASSIGN: "true" # Optional: assign issues to the committer AUTO_ASSIGN: "true" # Optional: assign issues to the committer

View File

@ -17,7 +17,24 @@ class GitHubClient(Client):
self.base_url = f'{self.github_url}/' self.base_url = f'{self.github_url}/'
self.repos_url = f'{self.base_url}repos/' self.repos_url = f'{self.base_url}repos/'
self.repo = os.getenv('INPUT_REPO') self.repo = os.getenv('INPUT_REPO')
self.target_repo = os.getenv('INPUT_TARGET_REPO', self.repo) # Default to current repo if not specified 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.before = os.getenv('INPUT_BEFORE')
self.sha = os.getenv('INPUT_SHA') self.sha = os.getenv('INPUT_SHA')
self.commits = json.loads(os.getenv('INPUT_COMMITS')) or [] 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. # 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}' diff_url = f'{self.repos_url}{self.repo}/compare/{self.before}...{self.sha}'
elif len(self.commits) == 1: elif len(self.commits) == 1:
# There is only one commit. # There is only one commit - compare against empty tree to get all files
diff_url = f'{self.repos_url}{self.repo}/commits/{self.sha}' diff_url = f'{self.repos_url}{self.repo}/compare/4b825dc642cb6eb9a060e54bf8d69288fbee4904...{self.sha}'
elif len(self.commits) > 1: elif len(self.commits) > 1:
# There are several commits: compare with the oldest one. # There are several commits: compare with the oldest one.
oldest = sorted(self.commits, key=self._get_timestamp)[0]['id'] oldest = sorted(self.commits, key=self._get_timestamp)[0]['id']
diff_url = f'{self.repos_url}{self.repo}/compare/{oldest}...{self.sha}' diff_url = f'{self.repos_url}{self.repo}/compare/{oldest}...{self.sha}'
else: 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 = { diff_headers = {
'Accept': 'application/vnd.github.v3.diff', 'Accept': 'application/vnd.github.v3.diff',

206
README.md
View File

@ -36,107 +36,135 @@ See [Upgrading](#upgrading) for breaking changes.
## Usage ## 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 ```yaml
def hello_world(): name: "TODO to Issue"
# TODO: Come up with a more imaginative greeting on: ["push"]
print('Hello world!') 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 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:
def hello_world():
# TODO: Come up with a more imaginative greeting
# Everyone uses hello world and it's boring.
print('Hello world!')
```
### Cross-Repository Issues ```yaml
name: "TODO to Issue"
The action supports creating issues in a different repository than the one where the TODO comments are found. This requires: on: ["push"]
jobs:
1. Setting the `TARGET_REPO` input to specify the target repository (e.g., "owner/repo") build:
runs-on: "ubuntu-latest"
2. Providing appropriate authentication: steps:
- For public repositories or when the target repository is in the same organization, use the default `GITHUB_TOKEN`: - uses: actions/checkout@v4
```yaml
- uses: alstr/todo-to-issue-action@v5
with: with:
TARGET_REPO: "my-org/target-repo" fetch-depth: 0
``` - name: "TODO to Issue"
- For private repositories, provide GitHub App credentials: uses: "alstr/todo-to-issue-action@v5"
```yaml
- uses: alstr/todo-to-issue-action@v5
with: with:
TARGET_REPO: "target-org/target-repo" TARGET_REPO: "gitops-repo"
APP_ID: ${{ secrets.APP_ID }} APP_ID: ${{ secrets.APP_ID }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
OWNER: "target-org" OWNER: "your-org"
```
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
### GitHub App Setup
To use the action with private repositories, you'll need to:
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
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)
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"
As per the [Google Style Guide](https://google.github.io/styleguide/cppguide.html#TODO_Comments), you can provide a _reference_ after the TODO identifier:
```python
def hello_world():
# TODO(@alstr): Come up with a more imaginative greeting
# This will assign the issue to alstr.
print('Hello world!')
# TODO(!urgent): This is wrong
# This will add an 'urgent' label.
assert 1 + 1 == 3
# TODO(#99): We need error handling here
# This will add the comment to the existing issue 99.
greeting_time = datetime.fromisoformat(date_string)
# TODO(language): Localise this string
# This will prepend the reference to the issue title
dialogue = "TODO or not TODO, that is the question."
``` ```
Only one reference can be provided. Should you wish to further configure the issue, you can do so via ## Configuration
[TODO Options](#todo-options).
### Required for Cross-Repository Usage
| 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 |
### Optional Inputs
| 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 (For Cross-Repository Usage)
If you want to create issues in a different repository, you'll need to create a GitHub App:
### 1. Create a GitHub App
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`
### 2. Set Permissions
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)
### 3. Generate and Store Secrets
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
### 4. Install the App
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"
```
### 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 ## TODO Options

View File

@ -5,18 +5,19 @@ runs:
using: "composite" using: "composite"
steps: steps:
- name: Extract repository names - name: Extract repository names
if: ${{ inputs.TARGET_REPO != '' && inputs.APP_ID != '' }} if: ${{ inputs.TARGET_REPO != '' && inputs.OWNER != '' && inputs.APP_ID != '' }}
id: extract-repos id: extract-repos
run: | run: |
SOURCE_REPO_NAME=$(echo "${{ inputs.REPO }}" | cut -d'/' -f2) 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 "source-repo-name=$SOURCE_REPO_NAME" >> $GITHUB_OUTPUT
echo "target-repo-name=$TARGET_REPO_NAME" >> $GITHUB_OUTPUT echo "target-repo-name=$TARGET_REPO_NAME" >> $GITHUB_OUTPUT
echo "repositories=$SOURCE_REPO_NAME,$TARGET_REPO_NAME" >> $GITHUB_OUTPUT echo "repositories=$SOURCE_REPO_NAME,$TARGET_REPO_NAME" >> $GITHUB_OUTPUT
shell: bash shell: bash
- name: Generate token for private repository access - 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 id: generate-token
uses: actions/create-github-app-token@v2 uses: actions/create-github-app-token@v2
with: with:
@ -80,7 +81,7 @@ inputs:
required: false required: false
default: ${{ github.token }} default: ${{ github.token }}
TARGET_REPO: 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 required: false
APP_ID: APP_ID:
description: "GitHub App ID for generating tokens to access private repositories" 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)" description: "Private key for the GitHub App (do not enter the actual secret)"
required: false required: false
OWNER: 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 required: false
CLOSE_ISSUES: CLOSE_ISSUES:
description: "Optional input specifying whether to attempt to close an issue when a TODO is removed" description: "Optional input specifying whether to attempt to close an issue when a TODO is removed"
required: false required: false
default: true default: "true"
AUTO_P: AUTO_P:
description: "For multiline TODOs, format each line as a new paragraph when creating the issue" description: "For multiline TODOs, format each line as a new paragraph when creating the issue"
required: false required: false
default: true default: "true"
PROJECT: PROJECT:
description: "User or organization project to link issues to, format 'project_type/owner/project_name'" description: "User or organization project to link issues to, format 'project_type/owner/project_name'"
required: false required: false
@ -111,7 +112,7 @@ inputs:
AUTO_ASSIGN: AUTO_ASSIGN:
description: "Automatically assign new issues to the user who triggered the action" description: "Automatically assign new issues to the user who triggered the action"
required: false required: false
default: false default: "false"
ACTOR: ACTOR:
description: "The username of the person who triggered the action (automatically set)" description: "The username of the person who triggered the action (automatically set)"
required: false required: false
@ -133,15 +134,15 @@ inputs:
ESCAPE: ESCAPE:
description: "Escape all special Markdown characters" description: "Escape all special Markdown characters"
required: false required: false
default: true default: "true"
LANGUAGES: LANGUAGES:
description: "A collection of comma-delimited URLs or local paths for custom language files" description: "A collection of comma-delimited URLs or local paths for custom language files"
required: false required: false
NO_STANDARD: NO_STANDARD:
description: "Exclude loading the default 'syntax.json' and 'languages.yml' files from the repository" description: "Exclude loading the default 'syntax.json' and 'languages.yml' files from the repository"
required: false required: false
default: false default: "false"
INSERT_ISSUE_URLS: INSERT_ISSUE_URLS:
description: "Whether the action should insert the URL for a newly-created issue into the associated TODO comment" description: "Whether the action should insert the URL for a newly-created issue into the associated TODO comment"
required: false required: false
default: false default: "false"