From 8fc972d908d82a725b061cc5235e847b531067f6 Mon Sep 17 00:00:00 2001 From: Jimmy Briggs Date: Sat, 14 Sep 2024 16:09:03 -0400 Subject: [PATCH 01/10] Add NEWS.md --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 NEWS.md diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..79e1610 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,3 @@ +# noclocksr (development version) + +* Initial CRAN submission. From fd70ef13434b5385d397b171bba428f77fd846c4 Mon Sep 17 00:00:00 2001 From: Jimmy Briggs Date: Sat, 14 Sep 2024 16:10:10 -0400 Subject: [PATCH 02/10] feat: add dependabot config --- .github/dependabot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..27a5243 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + assignees: + - "jimbrig" From 9ba1a390cbd42744ae80de79da472369df300a93 Mon Sep 17 00:00:00 2001 From: Jimmy Briggs Date: Sat, 14 Sep 2024 16:10:32 -0400 Subject: [PATCH 03/10] feat: add GHA for R package dependencies --- .github/actions/dependencies/action.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/actions/dependencies/action.yml diff --git a/.github/actions/dependencies/action.yml b/.github/actions/dependencies/action.yml new file mode 100644 index 0000000..3b8eb57 --- /dev/null +++ b/.github/actions/dependencies/action.yml @@ -0,0 +1,9 @@ +name: Dependencies + +runs: + using: 'composite' + steps: + - name: Install package dependencies 📄 + run: | + pak::local_install_deps(".", upgrade=FALSE, ask=FALSE, dependencies = TRUE) + shell: Rscript {0} From e83b470c3ce5928543144f0c4128a53b8b0b74d1 Mon Sep 17 00:00:00 2001 From: Jimmy Briggs Date: Sat, 14 Sep 2024 16:11:04 -0400 Subject: [PATCH 04/10] feat: add GHA workflows --- .github/cliff.toml | 144 +++++++++++++++++++--------- .github/workflows/changelog.yml | 44 +++++---- .github/workflows/check.yml | 30 ++++++ .github/workflows/coverage.yml | 62 ++++++++++++ .github/workflows/document.yml | 47 +++++++++ .github/workflows/lint.yml | 35 +++++++ .github/workflows/news.yml | 52 ++++++++++ .github/workflows/pull-requests.yml | 85 ++++++++++++++++ .github/workflows/style.yml | 78 +++++++++++++++ 9 files changed, 513 insertions(+), 64 deletions(-) create mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/coverage.yml create mode 100644 .github/workflows/document.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/news.yml create mode 100644 .github/workflows/pull-requests.yml create mode 100644 .github/workflows/style.yml diff --git a/.github/cliff.toml b/.github/cliff.toml index 2ce9f34..bdef9f4 100644 --- a/.github/cliff.toml +++ b/.github/cliff.toml @@ -1,64 +1,120 @@ -# configuration file for git-cliff -# see for default -# see also: +# No Clocks, LLC Custom Configuration Template File for `git-cliff` + [changelog] + +# remove the leading and trailing whitespace from the template +trim = true + +# header header = """ -# Changelog -*All notable changes to this project will be documented in this file.*\n +# Changelog\n +> All notable changes to this project will be documented in this file. The format is based on +[Keep a Changelog](http://keepachangelog.com/) and this project adheres to +[Semantic Versioning](http://semver.org/).\n """ + +# body - see https://tera.netlify.app/docs/#introduction body = """ {% if version %}\ - ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + ## [{{ version | trim_start_matches(pat="v") }}]\ + {% if previous %}\ + {% if previous.version %}\ + (REPOSITORY_URL/compare/{{ previous.version }}...{{ version }})\ + {% else %}\ + (REPOSITORY_URL/tree/{{ version }})\ + {% endif %}\ + {% endif %}\ + - ({{ timestamp | date(format="%Y-%m-%d") }}) {% else %}\ ## [Unreleased] {% endif %}\ {% for group, commits in commits | group_by(attribute="group") %} - ### {{ group | upper_first }} - {% for commit in commits %} - - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ - {% endfor %} + ## {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}\ + **{{commit.scope}}:** \ + {% endif %}\ + {% if '```' in commit.message %}\ + {{ commit.message | upper_first }}\n\ + ([{{ commit.id | truncate(length=7, end="") }}](REPOSITORY_URL/commit/{{ commit.id }})) - ({{ commit.author.name }})\ + {% else %}\ + {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](REPOSITORY_URL/commit/{{ commit.id }})) - ({{ commit.author.name }})\ + {% endif %}\ + {% if commit.breaking %}\ + {% for breakingChange in commit.footers %}\ + \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% endfor %}\ + {% endif %}\ + {% endfor %} {% endfor %}\n """ -trim = true + footer = """ *** *Changelog generated by [git-cliff](https://github.com/orhun/git-cliff).* +*** """ + [git] conventional_commits = true filter_unconventional = true -commit_parsers = [ - { message = "^feat", group = "Features"}, - { message = "^fix", group = "Bug Fixes"}, - { message = "^bug", group = "Bug Fixes"}, - { message = "^doc", group = "Documentation"}, - { message = "^docs", group = "Documentation"}, - { message = "^perf", group = "Performance"}, - { message = "^app", group = "Application"}, - { message = "^api", group = "API"}, - { message = "^data", group = "Data"}, - { message = "^db", group = "Database"}, - { message = "^refactor", group = "Refactoring"}, - { message = "^style", group = "Styling"}, - { message = "^test", group = "Testing"}, - { message = "^setup", group = "Infrastructure"}, - { message = "^infra", group = "Infrastructure"}, - { message = "^meta", group = "Meta"}, - { message = "^config", group = "Configuration"}, - { message = "^design", group = "Design"}, - { message = "^clean", group = "Cleanup"}, - { message = "^unit", group = "Testing"}, - { message = "^enhance", group = "Features"}, - { message = "^cicd", group = "DevOps"}, - { message = "^config", group = "Configuration"}, - { message = "^deploy", group = "Deployment"}, - { message = "^chore\\(release\\): prepare for", skip = true}, - { message = "^chore", group = "Miscellaneous Tasks"}, - { body = ".*security", group = "Security"}, -] -filter_commits = false +filter_commits = true tag_pattern = "v[0-9]*" -skip_tags = "v0.1.0-beta.1" +skip_tags = "v0.0.0.9999" # "v0.1.0-beta.1" ignore_tags = "" -topo_order = false -sort_commits = "oldest" +date_order = true +topo_order = true +sort_commits = "newest" # "oldest" +split_commits = false +protect_breaking_commits = true +# limit_commits = 42 +commit_preprocessors = [ + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](REPOSITORY_URL/issues/${2}))"}, + { pattern = "Merge pull request #([0-9]+) from [^ ]+", replace = "PR [#${1}](REPOSITORY_URL/pull/${1}):"}, +] +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^bug", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^docs", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^app", group = "Application" }, + { message = "^api", group = "API" }, + { message = "^data", group = "Data" }, + { message = "^db", group = "Database" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^style", group = "Styling" }, + { message = "^test", group = "Testing" }, + { message = "^setup", group = "Setup" }, + { message = "^infra", group = "Infrastructure" }, + { message = "^meta", group = "Meta" }, + { message = "^config", group = "Configuration" }, + { message = "^design", group = "Design" }, + { message = "^clean", group = "Cleanup" }, + { message = "^unit", group = "Testing" }, + { message = "^enhance", group = "Features" }, + { message = "^cicd", group = "DevOps" }, + { message = "^config", group = "Configuration" }, + { message = "^deploy", group = "Deployment" }, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore", group = "Miscellaneous Tasks", skip = true }, + { body = ".*security", group = "Security" }, +] + + +# ------------------------------------------------------------------------------ +# parse the commits based on https://www.conventionalcommits.org +# filter out the commits that are not conventional +# process each line of a commit as an individual commit +# regex for preprocessing the commit messages +# regex for parsing and grouping commits +# protect breaking changes from being skipped due to matching a skipping commit_parser +# filter out the commits that are not matched by commit parsers +# glob pattern for matching git tags +# regex for skipping tags +# regex for ignoring tags +# sort the tags chronologically +# sort the commits inside sections by oldest/newest order +# limit the number of commits included in the changelog +# ------------------------------------------------------------------------------ diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 83ad6f4..7e15b64 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -1,13 +1,13 @@ -name: Generate CHANGELOG.md +name: Generate Changelog on: workflow_dispatch: + workflow_call: push: - branches: - - main - - develop + branches: [ "main" ] + pull_request: jobs: changelog: - name: Generate changelog + name: Generate Changelog runs-on: ubuntu-latest steps: - name: Checkout @@ -15,20 +15,24 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate a changelog - uses: orhun/git-cliff-action@v1 + - name: Run Git Cliff + uses: tj-actions/git-cliff@v1.5.0 id: git-cliff with: - config: ./.github/cliff.toml - args: --verbose - env: - OUTPUT: ./CHANGELOG.md - - - name: Print the changelog - run: cat "${{ steps.git-cliff.outputs.changelog }}" - - - name: Commit and Push Changes - uses: actions-js/push@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} + args: "--verbose" + output: "CHANGELOG.md" + template-config: "./.github/cliff.toml" + - name: Print Changelog + id: print-changelog + run: | + cat "CHANGELOG.md" + # Commit and push the updated changelog, IF not a pull request + - name: Commit and Push Changelog + if: github.event_name != 'pull_request' + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + set +e + git add CHANGELOG.md + git commit -m "[chore]: update changelog" + git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git "main" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..49e8333 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,30 @@ +name: Check 📦 + +on: + workflow_call: + +concurrency: + group: check-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + check: + name: ${{ vars.CI_IMAGE }} + runs-on: ubuntu-latest + container: + image: ${{ vars.CI_IMAGE }} + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Checkout project ⬇️ + uses: actions/checkout@v4 + + - name: Install package dependencies 📄 + uses: noclocks/noclocksr/.github/actions/dependencies@main + + - name: Check 📦 + run: | + options(crayon.enabled = TRUE) + rcmdcheck::rcmdcheck(error_on = "error", args = "--no-tests") + shell: Rscript {0} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..75db95a --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,62 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + workflow_dispatch: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +name: Test Coverage + +permissions: read-all + +jobs: + test-coverage: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::covr, any::xml2 + needs: coverage + + - name: Test coverage + run: | + cov <- covr::package_coverage( + quiet = FALSE, + clean = FALSE, + install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") + ) + covr::to_cobertura(cov) + shell: Rscript {0} + + - uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: ${{ github.event_name != 'pull_request' && true || false }} + file: ./cobertura.xml + plugin: noop + disable_search: true + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Show testthat output + if: always() + run: | + ## -------------------------------------------------------------------- + find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true + shell: bash + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: coverage-test-failures + path: ${{ runner.temp }}/package diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml new file mode 100644 index 0000000..cd59734 --- /dev/null +++ b/.github/workflows/document.yml @@ -0,0 +1,47 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + workflow_dispatch: + push: + paths: ["R/**"] + +name: Document (Roxygen) + +permissions: read-all + +jobs: + document: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::roxygen2 + needs: roxygen2 + + - name: Document + run: roxygen2::roxygenise() + shell: Rscript {0} + + - name: Commit and push changes + run: | + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + git add man/\* NAMESPACE DESCRIPTION + git commit -m "Update documentation" || echo "No changes to commit" + git pull --ff-only + git push origin diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..905b748 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + workflow_dispatch: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +name: Lint + +permissions: read-all + +jobs: + lint: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::lintr, local::. + needs: lint + + - name: Lint + run: lintr::lint_package() + shell: Rscript {0} + env: + LINTR_ERROR_ON_LINT: true diff --git a/.github/workflows/news.yml b/.github/workflows/news.yml new file mode 100644 index 0000000..d600923 --- /dev/null +++ b/.github/workflows/news.yml @@ -0,0 +1,52 @@ +name: Generate NEWS.md + +on: + push: + branches: + - main + - develop + workflow_dispatch: + +permissions: read-all + +jobs: + generate_changelog: + uses: ./.github/workflows/changelog.yml + generate_news: + needs: [generate_changelog] + runs-on: ubuntu-latest + permissions: + contents: write + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install Dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packges: any::pkgload, any::markdown, any::xml2, any::stringr + needs: pkgload + + - name: Generate NEWS.md + run: | + Rscript -e 'pkgload::load_all(); noclocksr::generate_news(output_file = "NEWS.md", input_file = "CHANGELOG.md")' + + - name: Commit and push changes + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + set +e + git add NEWS.md + git commit -m "docs: Update NEWS.md" || echo "No changes to commit" + git pull --ff-only + git push origin diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml new file mode 100644 index 0000000..bc23a1e --- /dev/null +++ b/.github/workflows/pull-requests.yml @@ -0,0 +1,85 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + issue_comment: + types: [created] + +name: Pull Request Commands + +permissions: read-all + +jobs: + document: + if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }} + name: document + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/pr-fetch@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::roxygen2 + needs: pr-document + + - name: Document + run: roxygen2::roxygenise() + shell: Rscript {0} + + - name: commit + run: | + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + git add man/\* NAMESPACE + git commit -m 'Document' + + - uses: r-lib/actions/pr-push@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + style: + if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }} + name: style + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/pr-fetch@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - uses: r-lib/actions/setup-r@v2 + + - name: Install dependencies + run: install.packages("styler") + shell: Rscript {0} + + - name: Style + run: styler::style_pkg() + shell: Rscript {0} + + - name: commit + run: | + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + git add \*.R + git commit -m 'Style' + + - uses: r-lib/actions/pr-push@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 0000000..94f4241 --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,78 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + workflow_dispatch: + push: + paths: ["**.[rR]", "**.[qrR]md", "**.[rR]markdown", "**.[rR]nw", "**.[rR]profile"] + +name: Style + +permissions: read-all + +jobs: + style: + runs-on: ubuntu-latest + permissions: + contents: write + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::styler, any::roxygen2 + needs: styler + + - name: Enable styler cache + run: styler::cache_activate() + shell: Rscript {0} + + - name: Determine cache location + id: styler-location + run: | + cat( + "location=", + styler::cache_info(format = "tabular")$location, + "\n", + file = Sys.getenv("GITHUB_OUTPUT"), + append = TRUE, + sep = "" + ) + shell: Rscript {0} + + - name: Cache styler + uses: actions/cache@v4 + with: + path: ${{ steps.styler-location.outputs.location }} + key: ${{ runner.os }}-styler-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-styler- + ${{ runner.os }}- + + - name: Style + run: styler::style_pkg() + shell: Rscript {0} + + - name: Commit and push changes + run: | + if FILES_TO_COMMIT=($(git diff-index --name-only ${{ github.sha }} \ + | egrep --ignore-case '\.(R|[qR]md|Rmarkdown|Rnw|Rprofile)$')) + then + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + git commit ${FILES_TO_COMMIT[*]} -m "Style code (GHA)" + git pull --ff-only + git push origin + else + echo "No changes to commit." + fi From 9558f0dccb747f27766012aa9fdea829198efdc8 Mon Sep 17 00:00:00 2001 From: Jimmy Briggs Date: Sat, 14 Sep 2024 16:11:22 -0400 Subject: [PATCH 05/10] feat: add examples --- examples/ex_git_attributes.R | 5 +++++ examples/ex_git_config.R | 5 +++++ examples/ex_git_ignore.R | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 examples/ex_git_attributes.R create mode 100644 examples/ex_git_config.R create mode 100644 examples/ex_git_ignore.R diff --git a/examples/ex_git_attributes.R b/examples/ex_git_attributes.R new file mode 100644 index 0000000..72b5fce --- /dev/null +++ b/examples/ex_git_attributes.R @@ -0,0 +1,5 @@ +if (FALSE) { + + git_attributes() + +} diff --git a/examples/ex_git_config.R b/examples/ex_git_config.R new file mode 100644 index 0000000..08fdbd5 --- /dev/null +++ b/examples/ex_git_config.R @@ -0,0 +1,5 @@ +if (FALSE) { + + git_config() + +} diff --git a/examples/ex_git_ignore.R b/examples/ex_git_ignore.R new file mode 100644 index 0000000..2f3d5e2 --- /dev/null +++ b/examples/ex_git_ignore.R @@ -0,0 +1,5 @@ +if (FALSE) { + + git_ignore() + +} From bdb18314d12ea3e10067809576314eeab4a44d4d Mon Sep 17 00:00:00 2001 From: Jimmy Briggs Date: Sat, 14 Sep 2024 16:11:36 -0400 Subject: [PATCH 06/10] feat: add changelog and news templates --- .../github-workflows/changelog.yml.template | 38 ++++++++++++++ .../github-workflows/news.yml.template | 52 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 inst/templates/github-workflows/changelog.yml.template create mode 100644 inst/templates/github-workflows/news.yml.template diff --git a/inst/templates/github-workflows/changelog.yml.template b/inst/templates/github-workflows/changelog.yml.template new file mode 100644 index 0000000..b2433be --- /dev/null +++ b/inst/templates/github-workflows/changelog.yml.template @@ -0,0 +1,38 @@ +name: Automate Changelog +on: + workflow_dispatch: + workflow_call: + push: + branches: [ "main" ] + pull_request: +jobs: + changelog: + name: Generate Changelog + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: {{token}} + - name: Run Git Cliff + uses: tj-actions/git-cliff@v1.5.0 + id: git-cliff + with: + args: "--verbose" + output: "{{changelog_path}}" + template-config: "{{config_path}}" + - name: Print Changelog + id: print-changelog + run: | + cat "{{changelog_path}}" + # Commit and push the updated changelog, IF not a pull request + - name: Commit and Push Changelog + if: github.event_name != 'pull_request' + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + set +e + git add {{changelog_path}} + git commit -m "[chore]: update changelog" + git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git "main" diff --git a/inst/templates/github-workflows/news.yml.template b/inst/templates/github-workflows/news.yml.template new file mode 100644 index 0000000..3540871 --- /dev/null +++ b/inst/templates/github-workflows/news.yml.template @@ -0,0 +1,52 @@ +name: Generate NEWS.md + +on: + push: + branches: + - main + - develop + workflow_dispatch: + +permissions: read-all + +jobs: + generate_changelog: + uses: ./.github/workflows/changelog.yml + generate_news: + needs: [generate_changelog] + runs-on: ubuntu-latest + permissions: + contents: write + env: + GITHUB_PAT: {{token}} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup R + uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - name: Install Dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packges: any::pkgload, any::markdown, any::xml2, any::stringr + needs: pkgload + + - name: Generate NEWS.md + run: | + Rscript -e 'pkgload::load_all(); noclocksr::generate_news(output_file = "{{news_md_path}}", input_file = "{{changelog_path}}")' + + - name: Commit and push changes + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + set +e + git add {{news_md_path}} + git commit -m "docs: Update NEWS.md" || echo "No changes to commit" + git pull --ff-only + git push origin From 1cd8199e809dd9e267a71e82401d49a40cbf9fc2 Mon Sep 17 00:00:00 2001 From: Jimmy Briggs Date: Sat, 14 Sep 2024 16:12:10 -0400 Subject: [PATCH 07/10] feat: add NEWS, CHANGELOG, and utility functions --- R/pkg_changelog.R | 74 ++++++ R/pkg_news.R | 573 ++++++++++++++++++++++++++++++++++++++++++++++ R/utils_sys.R | 13 ++ 3 files changed, 660 insertions(+) create mode 100644 R/pkg_changelog.R create mode 100644 R/pkg_news.R create mode 100644 R/utils_sys.R diff --git a/R/pkg_changelog.R b/R/pkg_changelog.R new file mode 100644 index 0000000..850b8e1 --- /dev/null +++ b/R/pkg_changelog.R @@ -0,0 +1,74 @@ + +# ------------------------------------------------------------------------ +# +# Title : Package CHANGELOG.md +# By : Jimmy Briggs +# Date : 2024-09-14 +# +# ------------------------------------------------------------------------ + +# internal ---------------------------------------------------------------- + +.git_cliff_config_url <- "https://raw.githubusercontent.com/noclocks/.github/main/workflow-templates/cliff.template.toml" + +.git_cliff_changelog_gha_url <- "https://raw.githubusercontent.com/noclocks/.github/main/.github/workflows/changelog.yml" + +git_cliff <- function( + changelog_path = "CHANGELOG.md", + config_path = ".github/cliff.toml", + open = rlang::is_interactive() +) { + + cmd <- "git-cliff.exe" + + if (!test_sys_path(cmd)) { + rlang::abort( + c( + "{.code {cmd}} not found on the system's {.code PATH}." + ) + ) + } + + full_cmd <- paste0( + cmd, " -o ", changelog_path, " -c ", config_path + ) + + shell(full_cmd) + + cli::cli_alert_success("Git Cliff has successfully generated the changelog.") + if (open) { file.edit(changelog_path) } + + return(invisible(0)) + +} + +use_git_cliff <- function( + path = "CHANGELOG.md", + config = ".github/cliff.toml" +) { + + # get the cliff config file + if (!file.exists(config)) { + download.file(url = .git_cliff_config_url, destfile = config) + } + + # get the cliff changelog action file + if (!file.exists(".github/workflows/changelog.yml")) { + download.file(url = .git_cliff_changelog_gha_url, destfile = ".github/workflows/changelog.yml") + } + + # load the yaml file + yaml <- yaml::yaml.load_file(".github/workflows/changelog.yml") + + # ensure the output in yaml points to the changelog path + yaml$jobs$changelog$steps[[2]]$with$output <- path + + # ensure the template-config in yaml points to the config path + yaml$jobs$changelog$steps[[2]]$with$`template-config` <- config + + # write the yaml back to the file + yaml::write_yaml(yaml, ".github/workflows/changelog.yml") + + cli::cli_alert_success("Git Cliff has been successfully configured.") + +} diff --git a/R/pkg_news.R b/R/pkg_news.R new file mode 100644 index 0000000..f29c17e --- /dev/null +++ b/R/pkg_news.R @@ -0,0 +1,573 @@ +# ------------------------------------------------------------------------ +# +# Title : Package NEWS.md Management +# By : Jimmy Briggs +# Date : 2024-09-14 +# +# ------------------------------------------------------------------------ + +# generate_news ----------------------------------------------------------- + +#' Generate `NEWS.md` +#' +#' @description +#' These functions generate the R package's `NEWS.md` file. +#' +#' @details +#' - `generate_news()`: Generates a `NEWS.md` file by calling +#' `generate_news_from_changelog()` with default settings, +#' and will default to a generic `NEWS.md` file if no `CHANGELOG.md` +#' file is found. +#' +#' - `generate_news_from_changelog()`: Generates a `NEWS.md` file from a +#' pre-existing `CHANGELOG.md` file. It parses the Markdown content of +#' the `CHANGELOG.md` file, extracts version headers, sections, and their +#' content, and organizes them into a structured format suitable for +#' a typical R package's `NEWS.md` file. +#' +#' @param input_file Path to the `CHANGELOG.md` file. +#' @param output_file Path to the output `NEWS.md` file. +#' @param include_unreleased Logical indicating whether to include the +#' `[Unreleased]` section (default: `TRUE`). +#' @param remove_commits Logical indicating whether to remove commit hashes +#' and authors from the list items (default: `TRUE`). +#' @param version_pattern Regular expression pattern to match version headers +#' (default: `^\\[(Unreleased|\\d+\\.\\d+\\.\\d+(?:-\\w+)?)\\]`). +#' @param ordered_groups Character vector specifying the ordered groups for +#' sections in the `NEWS.md` file (default: `.ordered_groups`). +#' The default order is based on the significance of the groups. +#' @param skip_groups Character vector specifying the groups to skip when +#' generating the `NEWS.md` file (default: `NULL`). +#' @param section_name_mapping Named character vector to map section names +#' to custom names in the `NEWS.md` file (default: `NULL`). +#' The names should match the group names in the `CHANGELOG.md` file. +#' @param verbose Logical indicating whether to display messages (default: `TRUE`). +#' @param overwrite Logical indicating whether to overwrite existing `NEWS.md` file (default: `FALSE`). +#' @param pkg_name Package name (default: `NULL`). If `NULL`, reads from `DESCRIPTION`. +#' @param pkg_version Package version (default: `NULL`). If `NULL`, reads from `DESCRIPTION`. +#' @param pkg_path Path to the package directory containing `DESCRIPTION` (default: `NULL`). +#' @param ... Arguments passed on to `generate_news_from_changelog()` from +#' `generate_news()`. +#' +#' @return Both functions invisibly return the generated `news_content` +#' as a character vector. +#' +#' @export +#' +#' @seealso [use_github_action_news()] for implementing this into a GitHub Action +#' Workflow. +#' +#' @importFrom markdown markdownToHTML +#' @importFrom xml2 read_html xml_children xml_find_first xml_name xml_text xml_find_all +#' @importFrom stringr str_detect str_match str_replace str_trim +#' @importFrom rlang abort +#' @importFrom cli cli_alert_success cli_alert_info cli_alert_warning +#' @importFrom usethis use_news_md +#' +#' @examples +#' if (interactive()) { +#' +#' # Examples of using the `generate_news()` function: +#' generate_news() +#' +#' # Examples of using the `generate_news_from_changelog()` function: +#' +#' # Generate NEWS.md from CHANGELOG.md using all default settings +#' generate_news_from_changelog() +#' +#' # Specify custom input and output files +#' generate_news_from_changelog( +#' input_file = "path/to/your/CHANGELOG.md", +#' output_file = "path/to/your/NEWS.md" +#' ) +#' +#' # Overwrite the existing NEWS.md file +#' generate_news_from_changelog(overwrite = TRUE) +#' +#' # Exclude the 'Unreleased' section and keep commit hashes in the list items +#' generate_news_from_changelog(include_unreleased = FALSE, remove_commits = FALSE) +#' +#' # Skip certain sections +#' generate_news_from_changelog(skip_groups = c("Miscellaneous Tasks", "Meta")) +#' +#' # Map section names to custom names +#' generate_news_from_changelog( +#' section_name_mapping = c("Added" = "Features", "Fixed" = "Bug Fixes") +#' ) +#' +#' # Use custom ordered groups +#' custom_ordered_groups <- c( +#' "Breaking Changes", "Features", "Bug Fixes", "Documentation", "Testing" +#' ) +#' generate_news_from_changelog(ordered_groups = custom_ordered_groups) +#' +#' } +generate_news <- function( + output_file = "NEWS.md", + ... +) { + + if (!file.exists("CHANGELOG.md") && !file.exists("inst/CHANGELOG.md")) { + + cli::cli_alert_info( + c( + "No {.code CHANGELOG.md} file found in the package directory.", + "Using default {.code NEWS.md}." + ) + ) + + usethis::use_news_md(open = FALSE) + + res <- readLines("NEWS.md") + + return(invisible(res)) + + } + + clog_path <- if (file.exists("CHANGELOG.md")) { + "CHANGELOG.md" + } else { + "inst/CHANGELOG.md" + } + + cli::cli_alert_info( + c( + "Generating {.code NEWS.md} from {.code CHANGELOG.md}." + ) + ) + + res <- generate_news_from_changelog( + input_file = clog_path, + output_file = output_file, + ... + ) + + return(invisible(res)) + +} + +# generate_news_from_changelog -------------------------------------------- + +#' @rdname generate_news +#' +#' @export +generate_news_from_changelog <- function( + input_file = "CHANGELOG.md", + output_file = "NEWS.md", + include_unreleased = TRUE, + remove_commits = TRUE, + version_pattern = "^\\[(Unreleased|\\d+\\.\\d+\\.\\d+(?:-\\w+)?)\\]", + ordered_groups = .ordered_groups, + skip_groups = NULL, + section_name_mapping = NULL, + verbose = TRUE, + overwrite = FALSE, + pkg_name = NULL, + pkg_version = NULL, + pkg_path = NULL +) { + + # Check if input file exists and read content + if (!file.exists(input_file)) { + rlang::abort("Input file does not exist: {.path {input_file}}.") + } + if (verbose) { + cli::cli_alert_info("Reading {.path {input_file}}") + } + + changelog_content <- readLines(input_file) + if (length(changelog_content) == 0) { + rlang::abort("The {.code CHANGELOG.md} file is empty.") + } + + # Convert Markdown to HTML + if (verbose) { + cli::cli_alert_info("Converting Markdown to HTML") + } + changelog_html <- markdown::markdownToHTML( + text = changelog_content, + fragment.only = TRUE + ) + if (length(changelog_html) == 0) { + rlang::abort("Failed to convert CHANGELOG.md to HTML. The resulting HTML is empty.") + } + + + # Parse HTML content + if (verbose) { + cli::cli_alert_info("Parsing HTML content") + } + changelog_xml <- xml2::read_html(changelog_html) + + # Get all nodes under the body + body_nodes <- xml2::xml_children( + xml2::xml_find_first( + changelog_xml, + ".//body" + ) + ) + + # Initialize variables to store versions and their sections + versions <- list() + current_version <- NULL + current_section <- NULL + + heading_levels <- c("h2", "h3", "h4", "h5", "h6") + + # Loop through the body nodes to collect content under each version and section + for (node in body_nodes) { + node_name <- xml2::xml_name(node) + node_text <- xml2::xml_text(node) + + if (stringr::str_detect(node_text, version_pattern)) { + # New version header found + current_version <- node_text + versions[[current_version]] <- list() + current_section <- NULL + if (verbose) { + cli::cli_alert_info("Found version: {current_version}") + } + } else if (node_name %in% heading_levels && !is.null(current_version)) { + # New section under the current version + current_section <- node_text + versions[[current_version]][[current_section]] <- list() + if (verbose) { + cli::cli_alert_info(" Found section: {current_section}") + } + } else { + # Add node to the current section of the current version + if (!is.null(current_version) && !is.null(current_section)) { + versions[[current_version]][[current_section]] <- c( + versions[[current_version]][[current_section]], list(node) + ) + } + } + } + + # Read package name and version from DESCRIPTION or use provided values + if (is.null(pkg_name) || is.null(pkg_version)) { + if (is.null(pkg_path)) { + pkg_path <- "." + } + description_file <- file.path(pkg_path, "DESCRIPTION") + if (!file.exists(description_file)) { + rlang::abort( + c( + "{.code DESCRIPTION} file not found at {.path {description_file}}.", + "Please provide the {.arg pkg_name} and {.arg pkg_version} arguments, or specify the {.arg pkg_path}." + ) + ) + } + if (verbose) { + cli::cli_alert_info("Reading package info from {.path {description_file}}") + } + description_content <- read.dcf(description_file) + if (is.null(pkg_name)) { + pkg_name <- description_content[1, "Package"] + if (verbose) { + cli::cli_alert_info("Using package name from DESCRIPTION: {pkg_name}") + } + } + if (is.null(pkg_version)) { + pkg_version <- description_content[1, "Version"] + if (verbose) { + cli::cli_alert_info("Using package version from DESCRIPTION: {pkg_version}") + } + } + } else { + if (verbose) { + cli::cli_alert_info("Using provided package name: {pkg_name}") + cli::cli_alert_info("Using provided package version: {pkg_version}") + } + } + + # Initialize NEWS.md content + news_content <- character() + + # Process versions in order, placing '[Unreleased]' first if included + version_names <- names(versions) + + # Extract version numbers + version_numbers <- sapply(version_names, function(vn) { + vm <- stringr::str_match( + vn, + "^\\[(Unreleased|\\d+\\.\\d+\\.\\d+(?:-\\w+)?)\\]\\s*-?\\s*(.*)$" + ) + vm[1, 2] + }) + + # Identify 'Unreleased' and other versions + is_unreleased <- version_numbers == "Unreleased" + unreleased_versions <- version_names[is_unreleased] + other_versions <- version_names[!is_unreleased] + + # Convert other version numbers to package_version for sorting + other_version_numbers <- version_numbers[!is_unreleased] + parsed_versions <- package_version(other_version_numbers) + + # Order other versions in decreasing order + order_indices <- order(parsed_versions, decreasing = TRUE) + other_versions <- other_versions[order_indices] + + # Combine versions based on include_unreleased flag + if (include_unreleased && length(unreleased_versions) > 0) { + version_names_ordered <- c(unreleased_versions, other_versions) + } else { + version_names_ordered <- other_versions + } + + # Now process versions in order + for (version_name in version_names_ordered) { + # Extract version number and date if available + version_header <- version_name + version_match <- stringr::str_match( + version_name, + "^\\[(Unreleased|\\d+\\.\\d+\\.\\d+(?:-\\w+)?)\\]\\s*-?\\s*(.*)$" + ) + version_number <- version_match[1, 2] + version_date <- version_match[1, 3] + + # Skip 'Unreleased' if not included + if (!include_unreleased && version_number == "Unreleased") next + + # Build version header + if (!is.na(version_number)) { + version_header <- sprintf("# %s %s", pkg_name, version_number) + if (version_date != "") { + version_header <- sprintf("%s (%s)", version_header, version_date) + } + } else { + # If version header doesn't match expected pattern, use it as is + version_header <- sprintf("# %s %s", pkg_name, version_name) + } + + # Add version header to NEWS.md + news_content <- c(news_content, version_header, "") + + # Get the sections under the current version + sections <- versions[[version_name]] + + # Process sections in the order of significance + for (group_name in ordered_groups) { + # Skip groups if specified + if (!is.null(skip_groups) && group_name %in% skip_groups) next + + if (group_name %in% names(sections)) { + # Map section name if mapping is provided + mapped_name <- if (!is.null(section_name_mapping) && group_name %in% names(section_name_mapping)) { + section_name_mapping[[group_name]] + } else { + group_name + } + + # Add the section heading with appropriate heading level + news_section_header <- sprintf("## %s", mapped_name) + news_content <- c(news_content, news_section_header, "") + + # Process the nodes in sections[[group_name]] + for (node in sections[[group_name]]) { + node_name <- xml2::xml_name(node) + if (node_name %in% c("ul", "ol")) { + # List items + items <- xml2::xml_find_all(node, ".//li") + for (item in items) { + item_text <- xml2::xml_text(item) + # Optionally clean up item_text to remove commit hashes and authors + if (remove_commits) { + item_text <- stringr::str_replace( + item_text, + "\\s*\\(\\w{7}\\)\\s*-\\s*\\(.*?\\)\\s*$", + "" + ) + } + news_item <- paste0("* ", item_text) + news_content <- c(news_content, news_item) + } + } else { + # Other text, add as is if not empty + item_text <- stringr::str_trim(xml2::xml_text(node)) + if (item_text != "") { + news_item <- paste0("* ", item_text) + news_content <- c(news_content, news_item) + } + } + } + + # Add an empty line after each section + news_content <- c(news_content, "") + } + } + + # Process any remaining sections not in ordered_groups + remaining_sections <- setdiff(names(sections), ordered_groups) + for (section_name in remaining_sections) { + # Skip groups if specified + if (!is.null(skip_groups) && section_name %in% skip_groups) next + + # Map section name if mapping is provided + mapped_name <- if (!is.null(section_name_mapping) && section_name %in% names(section_name_mapping)) { + section_name_mapping[[section_name]] + } else { + section_name + } + + # Add the section heading + news_section_header <- sprintf("## %s", mapped_name) + news_content <- c(news_content, news_section_header, "") + + # Process the nodes in sections[[section_name]] + for (node in sections[[section_name]]) { + node_name <- xml2::xml_name(node) + if (node_name %in% c("ul", "ol")) { + items <- xml2::xml_find_all(node, ".//li") + for (item in items) { + item_text <- xml2::xml_text(item) + # Optionally clean up item_text to remove commit hashes and authors + if (remove_commits) { + item_text <- stringr::str_replace( + item_text, + "\\s*\\(\\w{7}\\)\\s*-\\s*\\(.*?\\)\\s*$", + "" + ) + } + news_item <- paste0("* ", item_text) + news_content <- c(news_content, news_item) + } + } else { + item_text <- stringr::str_trim(xml2::xml_text(node)) + if (item_text != "") { + news_item <- paste0("* ", item_text) + news_content <- c(news_content, news_item) + } + } + } + + # Add an empty line after each section + news_content <- c(news_content, "") + } + } + + # Print the news content if verbose + if (verbose) { + cli::cli_alert_info("Generated NEWS.md content:") + cat(news_content, sep = "\n") + } + + # Write the NEWS.md content to the output file + if (file.exists(output_file) && !overwrite) { + rlang::abort( + c( + "Output file already exists: {.path {output_file}}.", + "Use `overwrite = TRUE` to overwrite." + ) + ) + } + + writeLines(news_content, output_file) + if (verbose) { + cli::cli_alert_success("{.path {output_file}} file generated successfully.") + } + + return(invisible(news_content)) +} + + +# use_github_action_news ---------------------------------------- + +#' Generate GitHub Action Workflow for NEWS.md Generation +#' +#' @description +#' This function generates a GitHub Action workflow YAML file that automates +#' the generation of `NEWS.md` from `CHANGELOG.md` whenever changes are pushed +#' to the repository. +#' +#' @param file_name Name of the output workflow file (default: `news.yml`). +#' @param changelog_path Path to the `CHANGELOG.md` file (default: `CHANGELOG.md`). +#' @param config_path Path to the `cliff.toml` configuration file (default: `.github/cliff.toml`). +#' @param overwrite Logical indicating whether to overwrite the existing workflow file (default: `TRUE`). +#' @param verbose Logical indicating whether to display messages (default: `TRUE`). +#' +#' @return Invisibly returns `NULL`. +#' +#' @export +#' +#' @importFrom rlang abort +#' @importFrom cli cli_alert_success cli_alert_info +#' +#' @examples +#' if (interactive()) { +#' generate_github_action_workflow() +#' } +use_github_action_news <- function( + file_name = "news.yml", + news_md_path = "NEWS.md", + changelog_path = "CHANGELOG.md", + overwrite = TRUE, + verbose = TRUE +) { + + output_file <- file.path(".github", "workflows", file_name) + + if (file.exists(output_file) && !overwrite) { + rlang::abort( + c( + "Output file already exists: {.path {output_file}}.", + "Use `overwrite = TRUE` to overwrite." + ) + ) + } + + if (verbose) { + cli::cli_alert_info("Generating GitHub Action workflow at {.path {output_file}}") + } + + workflow_template <- "github-workflows/news.yml.template" + + workflow_template_params <- list( + changelog_path = changelog_path, + news_md_path = news_md_path, + token = "${{ secrets.GITHUB_TOKEN }}" + ) + + usethis::use_template( + workflow_template, + output_file, + data = workflow_template_params, + package = "noclocksr" + ) + + if (verbose) { + cli::cli_alert_success("GitHub Action workflow file created at {.path {output_file}}") + } + + return(invisible(NULL)) +} + +# Internal ---------------------------------------------------------------- + +.ordered_groups <- c( + "Features", + "Added", + "Bug Fixes", + "Fixed", + "Changed", + "Performance", + "Security", + "Refactoring", + "Testing", + "Documentation", + "Configuration", + "Design", + "Cleanup", + "Infrastructure", + "DevOps", + "Deployment", + "Application", + "API", + "Data", + "Database", + "Setup", + "Styling", + "Miscellaneous Tasks", + "Meta" +) diff --git a/R/utils_sys.R b/R/utils_sys.R new file mode 100644 index 0000000..0e1fb68 --- /dev/null +++ b/R/utils_sys.R @@ -0,0 +1,13 @@ +get_sys_path <- function() { + Sys.getenv("PATH") |> stringr::str_split(";") |> unlist() +} + +test_sys_path <- function(value) { + + if (Sys.which(value) == "") { + return(FALSE) + } + + return(TRUE) + +} From 64867308b4c00c0082e7809a82fd28a8c88086c5 Mon Sep 17 00:00:00 2001 From: Jimmy Briggs Date: Sat, 14 Sep 2024 16:12:29 -0400 Subject: [PATCH 08/10] test: Add NEWS.md generation tests --- tests/testthat/_snaps/pkg_news.md | 10 +++ tests/testthat/test-pkg_news.R | 102 ++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 tests/testthat/_snaps/pkg_news.md create mode 100644 tests/testthat/test-pkg_news.R diff --git a/tests/testthat/_snaps/pkg_news.md b/tests/testthat/_snaps/pkg_news.md new file mode 100644 index 0000000..c5e792c --- /dev/null +++ b/tests/testthat/_snaps/pkg_news.md @@ -0,0 +1,10 @@ +# generate_news_from_changelog handles missing DESCRIPTION + + Code + generate_news_from_changelog(input_file = file.path(tmp_dir, "CHANGELOG.md"), + output_file = output_file, verbose = FALSE, pkg_path = tmp_dir) + Condition + Error in `generate_news_from_changelog()`: + ! Output file already exists: {.path {output_file}}. + * Use `overwrite = TRUE` to overwrite. + diff --git a/tests/testthat/test-pkg_news.R b/tests/testthat/test-pkg_news.R new file mode 100644 index 0000000..6267985 --- /dev/null +++ b/tests/testthat/test-pkg_news.R @@ -0,0 +1,102 @@ +test_that("generate_news_from_changelog works with default parameters", { + # Create a temporary directory + tmp_dir <- tempdir() + + # Write a sample CHANGELOG.md + changelog_content <- c( + "# Changelog", + "", + "All notable changes to this project will be documented in this file.", + "", + "## [Unreleased]", + "", + "### Added", + "- New feature A", + "- New feature B", + "", + "### Fixed", + "- Bug fix 1", + "- Bug fix 2", + "", + "## [1.0.1] - 2023-09-14", + "", + "### Fixed", + "- Minor bug fix", + "", + "## [1.0.0] - 2023-09-13", + "", + "### Added", + "- Initial release", + "" + ) + writeLines(changelog_content, file.path(tmp_dir, "CHANGELOG.md")) + + # Write a sample DESCRIPTION file + description_content <- c( + "Package: testpackage", + "Type: Package", + "Title: Test Package", + "Version: 1.0.0", + "Authors@R: person('First', 'Last', email = 'first.last@example.com', role = c('aut', 'cre'))", + "Description: A test package.", + "License: MIT" + ) + writeLines(description_content, file.path(tmp_dir, "DESCRIPTION")) + + # Call the function + output_file <- file.path(tmp_dir, "NEWS.md") + generate_news_from_changelog( + input_file = file.path(tmp_dir, "CHANGELOG.md"), + output_file = output_file, + verbose = FALSE, + pkg_path = tmp_dir, + overwrite = TRUE + ) + + # Check that the NEWS.md file was created + expect_true(file.exists(output_file)) + + # Read the NEWS.md content + news_content <- readLines(output_file) + + # Check that the content contains expected entries + expect_true(any(grepl("# testpackage Unreleased", news_content))) + expect_true(any(grepl("## Added", news_content))) + expect_true(any(grepl("\\* New feature A", news_content))) + + # Clean up + unlink(tmp_dir) +}) + +test_that("generate_news_from_changelog handles missing DESCRIPTION", { + # Create a temporary directory + tmp_dir <- tempdir() + + # Write a sample CHANGELOG.md + changelog_content <- c( + "## [1.0.0] - 2023-09-13", + "", + "### Added", + "- Initial release", + "" + ) + writeLines(changelog_content, file.path(tmp_dir, "CHANGELOG.md")) + + # Call the function without a DESCRIPTION file + output_file <- file.path(tmp_dir, "NEWS.md") + + expect_snapshot( + x = { + generate_news_from_changelog( + input_file = file.path(tmp_dir, "CHANGELOG.md"), + output_file = output_file, + verbose = FALSE, + pkg_path = tmp_dir + ) + }, + error = TRUE + ) + + # Clean up + unlink(tmp_dir) +}) From 965ca21f586b43f916a7ab564c35339a7a4a2063 Mon Sep 17 00:00:00 2001 From: Jimmy Briggs Date: Sat, 14 Sep 2024 16:13:20 -0400 Subject: [PATCH 09/10] docs: cleanup and re-document --- DESCRIPTION | 11 +++++++++-- NAMESPACE | 31 +++++++++++++++++++++++++++++++ R/assets.R | 0 R/onLoad.R | 8 ++++---- man/noclocksr-package.Rd | 23 +++++++++++++++++++++++ 5 files changed, 67 insertions(+), 6 deletions(-) delete mode 100644 R/assets.R create mode 100644 man/noclocksr-package.Rd diff --git a/DESCRIPTION b/DESCRIPTION index dc951ac..2b32df6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -22,14 +22,18 @@ Imports: gargle, gitdown, glue, + grDevices, here, htmltools, httr, httr2, jsonlite, keyring, + lubridate, markdown, metathis, + monochromeR, + paletter, parallel, pdftools, pkgdown, @@ -42,8 +46,11 @@ Imports: tibble, tibblify, tidyr, + togglr, + usethis, utils, - xml2 + xml2, + yaml Suggests: knitr, rmarkdown, @@ -56,4 +63,4 @@ Encoding: UTF-8 Language: en-US LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.2.9000 +RoxygenNote: 7.3.2 diff --git a/NAMESPACE b/NAMESPACE index 000c8d9..8b353f6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -15,21 +15,30 @@ export(entrace) export(error_cnd) export(extract_pdf_content) export(fetch_brand) +export(generate_news) +export(generate_news_from_changelog) export(get_brand_logos) export(get_favicon) export(get_gitignore) export(get_logo_file_name) +export(get_tracked_time) export(git_attributes) export(git_config) export(git_ignore) +export(hex_to_rgb) export(parse_pdf_content) export(process_pdfs) +export(rgb_to_hex) +export(rgba_to_hex) export(shiny_resume_body) export(shiny_resume_navbar) export(shiny_resume_page) +export(start_time_tracking) +export(stop_time_tracking) export(trace_back) export(tree) export(typography) +export(use_github_action_news) export(use_noclocks_meta) export(write_log) import(htmltools) @@ -37,6 +46,11 @@ import(pkgdown) importFrom(assertthat,assert_that) importFrom(cli,ansi_strip) importFrom(cli,cat_line) +importFrom(cli,cli_abort) +importFrom(cli,cli_alert_info) +importFrom(cli,cli_alert_success) +importFrom(cli,cli_alert_warning) +importFrom(cli,cli_bullets) importFrom(cli,cli_progress_bar) importFrom(cli,cli_progress_done) importFrom(cli,cli_progress_update) @@ -78,6 +92,7 @@ importFrom(httr2,req_url_path_append) importFrom(httr2,request) importFrom(httr2,resp_body_json) importFrom(jsonlite,fromJSON) +importFrom(lubridate,weeks) importFrom(markdown,markdownToHTML) importFrom(metathis,meta) importFrom(metathis,meta_social) @@ -93,11 +108,15 @@ importFrom(purrr,pmap_chr) importFrom(purrr,pwalk) importFrom(rlang,abort) importFrom(rlang,cnd_entrace) +importFrom(rlang,current_env) importFrom(rlang,entrace) importFrom(rlang,error_cnd) importFrom(rlang,trace_back) importFrom(stringr,boundary) +importFrom(stringr,str_detect) importFrom(stringr,str_extract) +importFrom(stringr,str_match) +importFrom(stringr,str_replace) importFrom(stringr,str_replace_all) importFrom(stringr,str_to_lower) importFrom(stringr,str_trim) @@ -112,4 +131,16 @@ importFrom(tibblify,tib_unspecified) importFrom(tibblify,tibblify) importFrom(tibblify,tspec_object) importFrom(tidyr,unnest) +importFrom(togglr,get_time_entries) +importFrom(togglr,get_toggl_api_token) +importFrom(togglr,set_toggl_api_token) +importFrom(togglr,toggl_start) +importFrom(togglr,toggl_stop) +importFrom(usethis,use_news_md) importFrom(utils,assignInMyNamespace) +importFrom(xml2,read_html) +importFrom(xml2,xml_children) +importFrom(xml2,xml_find_all) +importFrom(xml2,xml_find_first) +importFrom(xml2,xml_name) +importFrom(xml2,xml_text) diff --git a/R/assets.R b/R/assets.R deleted file mode 100644 index e69de29..0000000 diff --git a/R/onLoad.R b/R/onLoad.R index 8975dcd..0bc9455 100644 --- a/R/onLoad.R +++ b/R/onLoad.R @@ -4,13 +4,13 @@ #' These functions are run when the package is loaded or attached. .onAttach <- function( - libname = find.package("noclocksR"), - pkgname = "noclocksR" + libname = find.package("noclocksr"), + pkgname = "noclocksr" ) { vers <- as.character(utils::packageVersion(pkgname)) msg <- sprintf( - "Welcome to `noclocksR`! This is version: %s\n", + "Welcome to `noclocksr`! This is version: %s\n", vers ) @@ -38,7 +38,7 @@ # # .auth_env <<- rlang::env( # auth_state = gargle::init_AuthState( - # package = "noclocksR", + # package = "noclocksr", # client = oauth_client, # auth_active = TRUE # ) diff --git a/man/noclocksr-package.Rd b/man/noclocksr-package.Rd new file mode 100644 index 0000000..826b977 --- /dev/null +++ b/man/noclocksr-package.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/noclocksr-package.R +\docType{package} +\name{noclocksr-package} +\alias{noclocksr} +\alias{noclocksr-package} +\title{noclocksr: Internal Development at No Clocks, LLC} +\description{ +No Clocks, LLC packaged assets and workflows +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://noclocks.github.io/noclocksr/} + \item \url{https://docs.noclocks.dev/noclocksr/} +} + +} +\author{ +\strong{Maintainer}: Jimmy Briggs \email{jimmy.briggs@jimbrig.com} (\href{https://orcid.org/0000-0002-7489-8787}{ORCID}) + +} +\keyword{internal} From 5347e0a643512e20475362c45900ec4d16da9833 Mon Sep 17 00:00:00 2001 From: Jimmy Briggs Date: Sat, 14 Sep 2024 16:13:33 -0400 Subject: [PATCH 10/10] docs: add man pages for new functions --- man/generate_news.Rd | 126 ++++++++++++++++++++++++++++++++++ man/use_github_action_news.Rd | 38 ++++++++++ 2 files changed, 164 insertions(+) create mode 100644 man/generate_news.Rd create mode 100644 man/use_github_action_news.Rd diff --git a/man/generate_news.Rd b/man/generate_news.Rd new file mode 100644 index 0000000..aff6341 --- /dev/null +++ b/man/generate_news.Rd @@ -0,0 +1,126 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pkg_news.R +\name{generate_news} +\alias{generate_news} +\alias{generate_news_from_changelog} +\title{Generate \code{NEWS.md}} +\usage{ +generate_news(output_file = "NEWS.md", ...) + +generate_news_from_changelog( + input_file = "CHANGELOG.md", + output_file = "NEWS.md", + include_unreleased = TRUE, + remove_commits = TRUE, + version_pattern = "^\\\\[(Unreleased|\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:-\\\\w+)?)\\\\]", + ordered_groups = .ordered_groups, + skip_groups = NULL, + section_name_mapping = NULL, + verbose = TRUE, + overwrite = FALSE, + pkg_name = NULL, + pkg_version = NULL, + pkg_path = NULL +) +} +\arguments{ +\item{output_file}{Path to the output \code{NEWS.md} file.} + +\item{...}{Arguments passed on to \code{generate_news_from_changelog()} from +\code{generate_news()}.} + +\item{input_file}{Path to the \code{CHANGELOG.md} file.} + +\item{include_unreleased}{Logical indicating whether to include the +\verb{[Unreleased]} section (default: \code{TRUE}).} + +\item{remove_commits}{Logical indicating whether to remove commit hashes +and authors from the list items (default: \code{TRUE}).} + +\item{version_pattern}{Regular expression pattern to match version headers +(default: \verb{^\\\[(Unreleased|\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:-\\\\w+)?)\\\]}).} + +\item{ordered_groups}{Character vector specifying the ordered groups for +sections in the \code{NEWS.md} file (default: \code{.ordered_groups}). +The default order is based on the significance of the groups.} + +\item{skip_groups}{Character vector specifying the groups to skip when +generating the \code{NEWS.md} file (default: \code{NULL}).} + +\item{section_name_mapping}{Named character vector to map section names +to custom names in the \code{NEWS.md} file (default: \code{NULL}). +The names should match the group names in the \code{CHANGELOG.md} file.} + +\item{verbose}{Logical indicating whether to display messages (default: \code{TRUE}).} + +\item{overwrite}{Logical indicating whether to overwrite existing \code{NEWS.md} file (default: \code{FALSE}).} + +\item{pkg_name}{Package name (default: \code{NULL}). If \code{NULL}, reads from \code{DESCRIPTION}.} + +\item{pkg_version}{Package version (default: \code{NULL}). If \code{NULL}, reads from \code{DESCRIPTION}.} + +\item{pkg_path}{Path to the package directory containing \code{DESCRIPTION} (default: \code{NULL}).} +} +\value{ +Both functions invisibly return the generated \code{news_content} +as a character vector. +} +\description{ +These functions generate the R package's \code{NEWS.md} file. +} +\details{ +\itemize{ +\item \code{generate_news()}: Generates a \code{NEWS.md} file by calling +\code{generate_news_from_changelog()} with default settings, +and will default to a generic \code{NEWS.md} file if no \code{CHANGELOG.md} +file is found. +\item \code{generate_news_from_changelog()}: Generates a \code{NEWS.md} file from a +pre-existing \code{CHANGELOG.md} file. It parses the Markdown content of +the \code{CHANGELOG.md} file, extracts version headers, sections, and their +content, and organizes them into a structured format suitable for +a typical R package's \code{NEWS.md} file. +} +} +\examples{ +if (interactive()) { + +# Examples of using the `generate_news()` function: +generate_news() + +# Examples of using the `generate_news_from_changelog()` function: + +# Generate NEWS.md from CHANGELOG.md using all default settings +generate_news_from_changelog() + +# Specify custom input and output files +generate_news_from_changelog( + input_file = "path/to/your/CHANGELOG.md", + output_file = "path/to/your/NEWS.md" +) + +# Overwrite the existing NEWS.md file +generate_news_from_changelog(overwrite = TRUE) + +# Exclude the 'Unreleased' section and keep commit hashes in the list items +generate_news_from_changelog(include_unreleased = FALSE, remove_commits = FALSE) + +# Skip certain sections +generate_news_from_changelog(skip_groups = c("Miscellaneous Tasks", "Meta")) + +# Map section names to custom names +generate_news_from_changelog( + section_name_mapping = c("Added" = "Features", "Fixed" = "Bug Fixes") +) + +# Use custom ordered groups +custom_ordered_groups <- c( + "Breaking Changes", "Features", "Bug Fixes", "Documentation", "Testing" +) +generate_news_from_changelog(ordered_groups = custom_ordered_groups) + +} +} +\seealso{ +\code{\link[=use_github_action_news]{use_github_action_news()}} for implementing this into a GitHub Action +Workflow. +} diff --git a/man/use_github_action_news.Rd b/man/use_github_action_news.Rd new file mode 100644 index 0000000..d24f92e --- /dev/null +++ b/man/use_github_action_news.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pkg_news.R +\name{use_github_action_news} +\alias{use_github_action_news} +\title{Generate GitHub Action Workflow for NEWS.md Generation} +\usage{ +use_github_action_news( + file_name = "news.yml", + news_md_path = "NEWS.md", + changelog_path = "CHANGELOG.md", + overwrite = TRUE, + verbose = TRUE +) +} +\arguments{ +\item{file_name}{Name of the output workflow file (default: \code{news.yml}).} + +\item{changelog_path}{Path to the \code{CHANGELOG.md} file (default: \code{CHANGELOG.md}).} + +\item{overwrite}{Logical indicating whether to overwrite the existing workflow file (default: \code{TRUE}).} + +\item{verbose}{Logical indicating whether to display messages (default: \code{TRUE}).} + +\item{config_path}{Path to the \code{cliff.toml} configuration file (default: \code{.github/cliff.toml}).} +} +\value{ +Invisibly returns \code{NULL}. +} +\description{ +This function generates a GitHub Action workflow YAML file that automates +the generation of \code{NEWS.md} from \code{CHANGELOG.md} whenever changes are pushed +to the repository. +} +\examples{ +if (interactive()) { + generate_github_action_workflow() +} +}