diff --git a/.github/workflows/branch-policy.yml b/.github/workflows/branch-policy.yml new file mode 100644 index 0000000..e17a392 --- /dev/null +++ b/.github/workflows/branch-policy.yml @@ -0,0 +1,33 @@ +name: Branch Policy + +on: + pull_request: + branches: + - master + - develop + +jobs: + validate-branch: + name: Validate Branch Policy + runs-on: ubuntu-latest + steps: + - name: Check branch naming policy + run: | + TARGET="${{ github.base_ref }}" + SOURCE="${{ github.head_ref }}" + + if [ "$TARGET" = "master" ]; then + if [[ "$SOURCE" != release/* && "$SOURCE" != hotfix/* ]]; then + echo "PRs to master must come from release/* or hotfix/* branches (got: $SOURCE)" + exit 1 + fi + fi + + if [ "$TARGET" = "develop" ]; then + if [[ "$SOURCE" != feature/* && "$SOURCE" != bugfix/* && "$SOURCE" != chore/* && "$SOURCE" != "master" ]]; then + echo "PRs to develop must come from feature/*, bugfix/*, chore/*, or master branches (got: $SOURCE)" + exit 1 + fi + fi + + echo "Branch policy check passed ($SOURCE -> $TARGET)" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54fc76d..67c11a7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,15 +2,17 @@ name: CI on: push: - branches: - - master - - develop - - "release/**" - - "hotfix/**" + branches: [master, develop, 'release/**', 'hotfix/**'] + paths-ignore: + - 'README.md' + - 'CHANGELOG.md' + - 'LICENSE' pull_request: - branches: - - master - - develop + branches: [master, develop] + paths-ignore: + - 'README.md' + - 'CHANGELOG.md' + - 'LICENSE' permissions: contents: read @@ -20,17 +22,17 @@ defaults: shell: bash jobs: - build: - name: Build + test: + name: Build & Test runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 - - name: Setup dependencies - uses: jdx/mise-action@6d1e696aa24c1aa1bcc1adea0212707c71ab78a8 # v3 + - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3 - - name: Build and test + - name: Build env: HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache run: mise run test diff --git a/.github/workflows/gitflow-release.yml b/.github/workflows/gitflow-release.yml new file mode 100644 index 0000000..2c30705 --- /dev/null +++ b/.github/workflows/gitflow-release.yml @@ -0,0 +1,66 @@ +name: GitFlow Release + +on: + pull_request: + types: [closed] + branches: + - master + +jobs: + tag-and-backmerge: + if: | + github.event.pull_request.merged == true && + (startsWith(github.event.pull_request.head.ref, 'release/') || + startsWith(github.event.pull_request.head.ref, 'hotfix/')) + name: Tag & Back-merge + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: master + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version from branch name + id: version + run: | + branch="${{ github.event.pull_request.head.ref }}" + version="${branch#release/}" + version="${version#hotfix/}" + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Create and push tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then + echo "Tag v${{ steps.version.outputs.version }} already exists, skipping." + else + git tag "v${{ steps.version.outputs.version }}" + git push origin "v${{ steps.version.outputs.version }}" + fi + + - name: Create back-merge branch + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "chore/back-merge-${{ steps.version.outputs.version }}" + git push origin "chore/back-merge-${{ steps.version.outputs.version }}" + + - name: Open back-merge PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create \ + --base develop \ + --head "chore/back-merge-${{ steps.version.outputs.version }}" \ + --title "chore: back-merge master into develop after v${{ steps.version.outputs.version }}" \ + --body "Automated back-merge of \`master\` into \`develop\` following the \`${{ github.event.pull_request.head.ref }}\` release." \ + --label "chore" || true + gh pr merge "chore/back-merge-${{ steps.version.outputs.version }}" \ + --auto \ + --merge \ + --delete-branch diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml deleted file mode 100644 index 1b1669e..0000000 --- a/.github/workflows/label-issues.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Label Issues - -on: - issues: - types: [opened, edited] - -permissions: - issues: write - -jobs: - label: - name: Label by title prefix - runs-on: ubuntu-latest - steps: - - name: Apply label from title prefix - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 - with: - script: | - const title = context.payload.issue.title.toLowerCase(); - const issue = { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number }; - - const map = [ - { prefix: 'bug:', label: 'bug' }, - { prefix: 'fix:', label: 'bug' }, - { prefix: 'feat:', label: 'enhancement' }, - { prefix: 'feature:', label: 'enhancement' }, - { prefix: 'question:', label: 'question' }, - { prefix: 'docs:', label: 'docs' }, - { prefix: 'chore:', label: 'ci' }, - { prefix: 'ci:', label: 'ci' }, - ]; - - const match = map.find(({ prefix }) => title.startsWith(prefix)); - if (match) { - await github.rest.issues.addLabels({ ...issue, labels: [match.label] }); - } diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 41ef037..f72dc76 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,21 +1,52 @@ -name: Label PRs +name: Labeler on: - pull_request_target: + pull_request: types: [opened, synchronize, reopened] + issues: + types: [opened, edited] permissions: contents: read + issues: write pull-requests: write jobs: - label: - name: Label by changed files + # ── Label PRs by changed files ─────────────────────────────────────────────── + label-pr: + name: Label PR + if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - - name: Apply labels - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 + - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 with: repo-token: ${{ secrets.GITHUB_TOKEN }} configuration-path: .github/labeler.yml - sync-labels: true + sync-labels: false + + # ── Label issues by title prefix ───────────────────────────────────────────── + label-issue: + name: Label Issue + if: github.event_name == 'issues' + runs-on: ubuntu-latest + steps: + - name: Apply label from title prefix + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NUMBER: ${{ github.event.issue.number }} + TITLE: ${{ github.event.issue.title }} + run: | + title=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]') + label="" + + case "$title" in + bug:*|fix:*) label="bug" ;; + feat:*|feature:*) label="enhancement" ;; + question:*) label="question" ;; + docs:*) label="docs" ;; + chore:*|ci:*) label="ci" ;; + esac + + if [ -n "$label" ]; then + gh issue edit "$NUMBER" --repo "${{ github.repository }}" --add-label "$label" + fi diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6b5aa79..e2e9e99 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,9 +1,23 @@ -name: Create Release +name: Release on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+" + - 'v[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (e.g. v0.2.0)' + required: true + type: string + dry_run: + description: 'Dry run — build without publishing' + required: false + type: boolean + default: false + +env: + RELEASE_TAG: ${{ inputs.tag || github.ref_name }} permissions: contents: write @@ -13,82 +27,118 @@ defaults: shell: bash jobs: + # ── 1. Build & test ────────────────────────────────────────────────────────── + test: + name: Build & Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ env.RELEASE_TAG }} + + - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3 + + - name: Build and test + env: + HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache + run: mise run test + + # ── 2. Package & release ───────────────────────────────────────────────────── release: + name: GitHub Release + needs: test runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ env.RELEASE_TAG }} - - name: Setup dependencies - uses: jdx/mise-action@6d1e696aa24c1aa1bcc1adea0212707c71ab78a8 # v3 + - uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3 - name: Set version from tag id: version - run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" - - - name: Get short commit SHA - id: sha - run: echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + run: echo "version=${RELEASE_TAG#v}" >> "$GITHUB_OUTPUT" + env: + RELEASE_TAG: ${{ env.RELEASE_TAG }} - name: Package module (tar.gz) - id: package env: VERSION: ${{ steps.version.outputs.version }} - run: | - mise run package - echo "archive=lynko-${VERSION}.tar.gz" >> "$GITHUB_OUTPUT" + run: mise run package - name: Package module (zip) - id: zip env: VERSION: ${{ steps.version.outputs.version }} + run: mise run zip + + - name: Build release notes + id: notes + env: + VERSION: ${{ steps.version.outputs.version }} + RELEASE_TAG: ${{ env.RELEASE_TAG }} + REPO: ${{ github.repository }} run: | - mise run zip - echo "zip=lynko-${VERSION}.zip" >> "$GITHUB_OUTPUT" + # Extract the changelog section for this version + changelog=$(awk -v ver="$VERSION" ' + /^## \[/ { if (found) exit; if (index($0, "[" ver "]")) found=1; next } + found { print } + ' CHANGELOG.md | awk 'NF{found=NR} {lines[NR]=$0} END{for(i=1;i<=found;i++) print lines[i]}') - - name: Create GitHub Release - uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2 - with: - name: "lynko v${{ steps.version.outputs.version }}" - generate_release_notes: true - files: | - ${{ steps.package.outputs.archive }} - ${{ steps.zip.outputs.zip }} - body: | - ## Installation + cat > /tmp/release-notes.md << NOTES + ${changelog} + + --- + + ## Installation - ### Hugo Modules (recommended) + ### Hugo Modules (recommended) - Initialize Hugo modules in your site if you haven't already: + Initialize Hugo modules in your site if you haven't already: - ```sh - hugo mod init github.com/your-username/your-site - ``` + \`\`\`sh + hugo mod init github.com/your-username/your-site + \`\`\` - Add the import to `hugo.toml`: + Add the import to \`hugo.toml\`: - ```toml - [module] - [[module.imports]] - path = "github.com/haydenk/lynko" - ``` + \`\`\`toml + [module] + [[module.imports]] + path = "github.com/haydenk/lynko" + \`\`\` - Then fetch this exact release: + Then fetch this exact release: - ```sh - hugo mod get github.com/haydenk/lynko@v${{ steps.version.outputs.version }} - ``` + \`\`\`sh + hugo mod get github.com/haydenk/lynko@${RELEASE_TAG} + \`\`\` - Create the section page and configure your links: + Create the section page and configure your links: - ```sh - mkdir -p content/links - echo '+++\ntitle = "Links"\n+++' > content/links/_index.md - ``` + \`\`\`sh + mkdir -p content/links + printf '+++\ntitle = "Links"\nlayout = "lynko"\n+++\n' > content/links/_index.md + \`\`\` - Add a `[params.lynko]` block to your `hugo.toml` — see the [README](https://github.com/${{ github.repository }}/blob/master/README.md) for the full configuration reference. + Add a \`[params.lynko]\` block to your \`hugo.toml\` — see the [README](https://github.com/${REPO}/blob/master/README.md) for the full configuration reference. - ### Requirements + ### Requirements - - Hugo v0.121.0+ - - Go 1.22+ + - Hugo v0.121.0+ + - Go 1.22+ + NOTES + + - name: Create GitHub Release + if: ${{ !inputs.dry_run }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + RELEASE_TAG: ${{ env.RELEASE_TAG }} + run: | + gh release create "$RELEASE_TAG" \ + --title "lynko $RELEASE_TAG" \ + --notes-file /tmp/release-notes.md \ + "lynko-${VERSION}.tar.gz" \ + "lynko-${VERSION}.zip" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 2ea4b17..08af7bd 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,8 +1,8 @@ -name: Stale issues and PRs +name: Stale on: schedule: - - cron: "0 8 * * 1" # every Monday at 08:00 UTC + - cron: "0 8 * * 1" # every Monday at 08:00 UTC workflow_dispatch: permissions: @@ -11,41 +11,48 @@ permissions: jobs: stale: - name: Mark and close stale threads + name: Mark & close stale items runs-on: ubuntu-latest steps: - - name: Run stale action - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - # Issues + # ── Issues ────────────────────────────────────────────────────────── days-before-issue-stale: 60 days-before-issue-close: 14 stale-issue-label: stale - stale-issue-message: > - This issue has had no activity for 60 days and has been marked as stale. - It will be closed in 14 days unless there is further activity. - If this is still relevant, please leave a comment or remove the **stale** label. - close-issue-message: > - This issue was closed automatically after 14 days with no activity. - Feel free to reopen it if the problem persists. - - # PRs + stale-issue-message: | + This issue has been inactive for **60 days** and is being marked as stale. + + If this is still relevant, please leave a comment to keep it open — even a quick "still seeing this" helps. + Issues that remain inactive for another 14 days will be closed automatically. + close-issue-message: | + This issue has been closed automatically due to inactivity. + + If the problem persists or the feature is still needed, please open a new issue with up-to-date information. + Thank you for contributing! + close-issue-reason: not_planned + + # ── Pull Requests ──────────────────────────────────────────────────── days-before-pr-stale: 30 days-before-pr-close: 7 stale-pr-label: stale - stale-pr-message: > - This pull request has had no activity for 30 days and has been marked as stale. - It will be closed in 7 days unless there is further activity. - If this is still relevant, please rebase and push an update or leave a comment. - close-pr-message: > - This pull request was closed automatically after 7 days with no activity. - Feel free to reopen or open a new PR if you'd like to continue. - - # Never auto-close these + stale-pr-message: | + This pull request has been inactive for **30 days** and is being marked as stale. + + Please rebase, address any review feedback, or leave a comment if you plan to continue working on it. + PRs that remain inactive for another 7 days will be closed. + close-pr-message: | + This pull request has been closed automatically due to inactivity. + + If you'd like to continue this work, please reopen the PR or open a new one. + + # ── Exempt labels ──────────────────────────────────────────────────── exempt-issue-labels: "pinned,roadmap,good first issue" exempt-pr-labels: "pinned" - exempt-all-issues-assignees: false - exempt-all-pr-assignees: false + # ── Limits ─────────────────────────────────────────────────────────── + operations-per-run: 60 + remove-stale-when-updated: true + ascending: true diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index eb9056e..3ee443f 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -1,4 +1,4 @@ -name: Welcome new contributors +name: Welcome on: issues: @@ -15,33 +15,56 @@ jobs: name: Greet first-time contributors runs-on: ubuntu-latest steps: - - name: First issue greeting + - name: Greet first-time issue author if: github.event_name == 'issues' - uses: actions/first-interaction@3c71ce730280171fd1cfb57c00c774f8998586f7 # v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: > - Thanks for opening your first issue! 👋 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ACTOR: ${{ github.event.issue.user.login }} + NUMBER: ${{ github.event.issue.number }} + REPO: ${{ github.repository }} + OWNER: ${{ github.repository_owner }} + run: | + # Skip the repo owner + if [ "$ACTOR" = "$OWNER" ]; then exit 0; fi - A maintainer will take a look soon. In the meantime: + count=$(gh issue list --repo "$REPO" --author "$ACTOR" --state all --limit 100 --json number --jq 'length' 2>/dev/null || echo 0) + if [ "$count" -gt 1 ]; then exit 0; fi - - If this is a **bug**, please make sure you've included your Hugo version (`hugo version` - and the relevant `[params.lynko]` section of your `hugo.toml`. - - If this is a **question about Hugo itself** rather than the module, the - [Hugo Discourse forum](https://discourse.gohugo.io/) is a great resource. + gh issue comment "$NUMBER" --repo "$REPO" --body "$(cat < - Thanks for your first contribution to lynko! 🎉 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ACTOR: ${{ github.event.pull_request.user.login }} + NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + OWNER: ${{ github.repository_owner }} + run: | + # Skip the repo owner + if [ "$ACTOR" = "$OWNER" ]; then exit 0; fi + + count=$(gh pr list --repo "$REPO" --author "$ACTOR" --state all --limit 100 --json number --jq 'length' 2>/dev/null || echo 0) + if [ "$count" -gt 1 ]; then exit 0; fi + + gh pr comment "$NUMBER" --repo "$REPO" --body "$(cat <