From bb91f481d94950a1cb8b5a8337232e547f0c1e8d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:59:13 +0000 Subject: [PATCH] Sync organization files --- .github/label-commenter-config.yml | 8 +- .github/workflows/auto-tag-new-version.yml | 8 +- .github/workflows/close_stale_issue.yml | 41 ++--- .github/workflows/continuous-integration.yml | 5 + .github/workflows/forward-port.yml | 106 +++++++++++++ .github/workflows/label-commenter.yml | 7 +- .github/workflows/open-xml-update-pr.yml | 157 +++++++++++++++++++ .github/workflows/release.yml | 2 +- 8 files changed, 305 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/forward-port.yml create mode 100644 .github/workflows/open-xml-update-pr.yml diff --git a/.github/label-commenter-config.yml b/.github/label-commenter-config.yml index b0c1bb48..5f2918ef 100644 --- a/.github/label-commenter-config.yml +++ b/.github/label-commenter-config.yml @@ -10,16 +10,16 @@ labels: issue: body: | This issue has been closed as we only track bugs here. - + You can get community support on [forums](https://forum.glpi-project.org/) or you can consider [taking a subscription](https://glpi-project.org/subscriptions/) to get professional support. You can also [contact GLPI editor team](https://portal.glpi-network.com/contact-us) directly. - action: close + action: "close" - name: "feature suggestion" labeled: issue: body: | This issue has been closed as we only track bugs here. - + You can open a topic to discuss with community about this enhancement on [suggestion website](https://glpi.userecho.com/). You can also [contact GLPI editor team](https://portal.glpi-network.com/contact-us) directly if you are willing to sponsor this feature. - action: close + action: "close" diff --git a/.github/workflows/auto-tag-new-version.yml b/.github/workflows/auto-tag-new-version.yml index 1fa94344..89c03d7b 100644 --- a/.github/workflows/auto-tag-new-version.yml +++ b/.github/workflows/auto-tag-new-version.yml @@ -8,9 +8,15 @@ on: paths: - "setup.php" +# Tags are pushed with AUTOTAG_TOKEN +permissions: + contents: "read" + jobs: auto-tag-new-version: name: "Automatically tag new version" uses: "glpi-project/plugin-release-workflows/.github/workflows/auto-tag-new-version.yml@v1" + with: + bot-client-id: "${{ vars.GLPI_PROJECT_BOT_CLIENT_ID }}" secrets: - github-token: "${{ secrets.AUTOTAG_TOKEN }}" + bot-private-key: "${{ secrets.GLPI_PROJECT_BOT_PRIVATE_KEY }}" diff --git a/.github/workflows/close_stale_issue.yml b/.github/workflows/close_stale_issue.yml index b0f952bc..c1fbc33e 100644 --- a/.github/workflows/close_stale_issue.yml +++ b/.github/workflows/close_stale_issue.yml @@ -1,35 +1,36 @@ -name: 'Close stale issues' +name: "Close stale issues and pull requests" + on: schedule: - - cron: '0 8 * * *' + - cron: "0 8 * * *" jobs: stale: if: github.repository == 'pluginsGLPI/tag' permissions: - issues: write # for actions/stale to close stale issues - runs-on: ubuntu-latest + issues: "write" # for actions/stale to close stale issues + pull-requests: "write" # for actions/stale to close stale pull requests + runs-on: "ubuntu-latest" steps: - - uses: actions/stale@v11 + - uses: "actions/stale@v11" with: stale-issue-message: >- - There has been no activity on this issue for some time and therefore it is considered stale - and will be closed automatically in 10 days. - - - If this issue is related to a bug, please try to reproduce on latest release. If the problem persist, + There has been no activity on this issue for 60 days and therefore it is considered stale + and will be closed automatically in 14 days. + + + If this issue is related to a bug, please try to reproduce on latest release. If the problem persists, feel free to add a comment to revive this issue. - - If it is related to a new feature, please open a topic to discuss with community about this enhancement - on [suggestion website](https://glpi.userecho.com/). - - - You may also consider taking a [subscription](https://glpi-project.org/subscriptions/) to get professionnal - support or [contact GLPI editor team](https://portal.glpi-network.com/contact-us) directly. - days-before-issue-stale: 15 - days-before-pr-stale: -1 # PR will be marked as stale manually. - days-before-close: 5 + stale-pr-message: >- + There has been no activity on this pull request for 90 days and therefore it is considered stale + and will be closed automatically in 14 days. + Feel free to add a comment or push new commits to keep it open. + days-before-issue-stale: 60 + days-before-pr-stale: 90 + days-before-close: 14 exempt-issue-labels: "bug,enhancement,question,security" # Issues with "bug", "enhancement", "question" or "security" labels will not be marked as stale + exempt-pr-labels: "security" # PR with "security" label will not be marked as stale + exempt-draft-pr: true # Draft PR are work in progress. exempt-all-milestones: true # Do not check issues/PR with defined milestone. ascending: true # First check older issues/PR. operations-per-run: 750 # Max API calls per run. diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1e719fe6..ef41c55a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -15,6 +15,11 @@ concurrency: group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true +# The changelog and plugin manifest checks read the pull request files +permissions: + contents: "read" + pull-requests: "read" + jobs: generate-ci-matrix: name: "Generate CI matrix" diff --git a/.github/workflows/forward-port.yml b/.github/workflows/forward-port.yml new file mode 100644 index 00000000..48329264 --- /dev/null +++ b/.github/workflows/forward-port.yml @@ -0,0 +1,106 @@ +name: "Forward-port" + +# `pull_request_target` workflows always run from the default branch, whatever the base branch of the pull request: +# this file only has to be present on the default branch. + +on: + pull_request_target: + types: + - "closed" + +jobs: + forward-port: + name: "Forward-port" + runs-on: "ubuntu-latest" + # Write operations use the app token: `GITHUB_TOKEN` only lists the protected branches. + permissions: + contents: "read" + # Pull requests opened by bots (dependabot, translations updates, ...) are handled on each branch separately, + # except the forward-port pull requests, so that a fix goes up the whole branches chain. + if: | + github.event.pull_request.merged + && (github.event.pull_request.user.type != 'Bot' || startsWith(github.event.pull_request.head.ref, 'forward-port/')) + && !contains(github.event.pull_request.labels.*.name, 'no forward-port') + steps: + # The branches chain is made of the protected `/bugfixes` branches, sorted by version, + # followed by the default branch: a pull request is forward-ported to the next branch of the chain. + - name: "Find target branch" + id: "target" + env: + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} + BASE_BRANCH: ${{ github.event.pull_request.base.ref }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + # Fetched in its own assignment, so that an API failure fails the job instead of being swallowed. + PROTECTED_BRANCHES=$(gh api --paginate "repos/${REPOSITORY}/branches?protected=true&per_page=100" --jq '.[].name') + BRANCHES=$( + echo "${PROTECTED_BRANCHES}" | grep -E '^[0-9]+(\.[0-9]+)*/bugfixes$' | sort -V + echo "${DEFAULT_BRANCH}" + ) + TARGET_BRANCH=$(echo "${BRANCHES}" | awk -v base="${BASE_BRANCH}" 'found { print; exit } $0 == base { found = 1 }') + if [[ -z "${TARGET_BRANCH}" ]]; then + echo "No branch to forward-port \`${BASE_BRANCH}\` to." + else + echo "Forward-port from \`${BASE_BRANCH}\` to \`${TARGET_BRANCH}\`." + fi + echo "branch=${TARGET_BRANCH}" >> "$GITHUB_OUTPUT" + + # Do not stack the `[]` prefixes when a forward-port pull request is itself forward-ported. + TITLE="${PR_TITLE}" + if [[ "${HEAD_BRANCH}" == forward-port/* ]]; then + TITLE="${TITLE#\[*\] }" + fi + # Random delimiter, so that no pull request title can end the block early. + DELIMITER=$(openssl rand -hex 16) + { + echo "title<<${DELIMITER}" + echo "${TITLE}" + echo "${DELIMITER}" + } >> "$GITHUB_OUTPUT" + + # Pull requests opened with the workflow `GITHUB_TOKEN` do not trigger the CI: the org bot app is used instead. + # No `workflows` permission, to keep workflow files writable only by humans: + # pull requests changing `.github/workflows/` fail to be pushed and must be forward-ported manually. + - name: "Generate app token" + if: steps.target.outputs.branch != '' + id: "app-token" + uses: "actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510" # v3.3.0 + with: + client-id: "${{ vars.GLPI_PROJECT_BOT_CLIENT_ID }}" + private-key: "${{ secrets.GLPI_PROJECT_BOT_PRIVATE_KEY }}" + permission-contents: "write" + permission-pull-requests: "write" + + # Only the default branch is checked out: the PR code is cherry-picked, never executed. + - name: "Checkout" + if: steps.target.outputs.branch != '' + uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1 + with: + token: "${{ steps.app-token.outputs.token }}" + + - name: "Create forward-port pull request" + if: steps.target.outputs.branch != '' + uses: "korthout/backport-action@8560fb503c275d433c56f05a2f64850093baa9f7" # v4.7.0 + with: + github_token: "${{ steps.app-token.outputs.token }}" + target_branches: ${{ steps.target.outputs.branch }} + label_pattern: "" + branch_name: "forward-port/${pull_number}-to-${target_branch}" + pull_title: "[${target_branch}] ${{ steps.target.outputs.title }}" + pull_description: |- + Forward-port of #${pull_number} to `${target_branch}`. + + ${pull_description} + # The author of a forward-port pull request is the bot: its assignees (the original author) are kept instead. + add_author_as_assignee: ${{ !startsWith(github.event.pull_request.head.ref, 'forward-port/') }} + copy_assignees: true + comment_style: "summary" + merge_commits: "skip" + # On conflict, open a draft PR containing the conflict, to be resolved by the PR author. + experimental: > + { + "conflict_resolution": "draft_commit_conflicts" + } diff --git a/.github/workflows/label-commenter.yml b/.github/workflows/label-commenter.yml index 9eb1235f..ab80f492 100644 --- a/.github/workflows/label-commenter.yml +++ b/.github/workflows/label-commenter.yml @@ -4,7 +4,6 @@ on: issues: types: - "labeled" - - "unlabeled" jobs: comment: @@ -14,7 +13,9 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: "actions/checkout@v7" + uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1 + with: + persist-credentials: false - name: "Label commenter" - uses: "peaceiris/actions-label-commenter@v1" + uses: "peaceiris/actions-label-commenter@f0dbbef043eb1b150b566db36b0bdc8b7f505579" # v1.10.0 diff --git a/.github/workflows/open-xml-update-pr.yml b/.github/workflows/open-xml-update-pr.yml new file mode 100644 index 00000000..0fec74ba --- /dev/null +++ b/.github/workflows/open-xml-update-pr.yml @@ -0,0 +1,157 @@ +name: "Open plugin XML update PR" + +# Synced files only reach the default branch, where workflow_run workflows are read from, unlike tag pushes from X.Y/bugfixes +on: + workflow_run: + workflows: + - "Automatically tag new version" + types: + - "completed" + +# The pull request is opened with the bot token +permissions: + contents: "read" + +jobs: + open-xml-update-pr: + if: github.repository == 'pluginsGLPI/tag' && github.event.workflow_run.conclusion == 'success' + name: "Open plugin XML update PR" + runs-on: "ubuntu-latest" + steps: + - name: "Generate bot token" + id: "bot-app-token" + uses: "actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510" # v3.3.0 + with: + client-id: "${{ vars.GLPI_PROJECT_BOT_CLIENT_ID }}" + private-key: "${{ secrets.GLPI_PROJECT_BOT_PRIVATE_KEY }}" + permission-contents: "write" + permission-pull-requests: "write" + - name: "Copy the tag version block to the default branch XML" + uses: "actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3" # v9.0.0 + env: + HEAD_SHA: "${{ github.event.workflow_run.head_sha }}" + DEFAULT_BRANCH: "${{ github.event.repository.default_branch }}" + with: + github-token: "${{ steps.bot-app-token.outputs.token }}" + script: | + const { owner, repo } = context.repo; + const base = process.env.DEFAULT_BRANCH + || (await github.rest.repos.get({ owner, repo })).data.default_branch; + + const readFile = async (path, ref) => { + try { + const { data } = await github.rest.repos.getContent({ owner, repo, path, ref }); + return Buffer.from(data.content, 'base64').toString('utf8'); + } catch (error) { + if (error.status !== 404) { + throw error; + } + return undefined; + } + }; + + // Same version extraction as the auto tag workflow, whose tag is named after it + const tag = (await readFile('setup.php', process.env.HEAD_SHA))?.match(/define\s*\(\s*['"]PLUGIN_[A-Z]+_VERSION['"]\s*,\s*['"]([^'"]+)['"]\s*\)/)?.[1]; + if (tag === undefined) { + core.info('No plugin version found in setup.php, skipping update.'); + return; + } + const tagRef = `refs/tags/${tag}`; + let ref; + try { + ({ data: ref } = await github.rest.git.getRef({ owner, repo, ref: `tags/${tag}` })); + } catch (error) { + if (error.status !== 404) { + throw error; + } + core.info(`No ${tag} tag, skipping update.`); + return; + } + // Any setup.php push succeeds the auto tag run, only a tag on the triggering commit is a fresh one + const tagSha = ref.object.type === 'tag' + ? (await github.rest.git.getTag({ owner, repo, tag_sha: ref.object.sha })).data.object.sha + : ref.object.sha; + if (tagSha !== process.env.HEAD_SHA) { + core.info(`${tag} was not created by this run, skipping update.`); + return; + } + + let xmlPath = 'plugin.xml'; + let tagXml = await readFile(xmlPath, tagRef); + if (tagXml === undefined) { + const pluginKey = (await readFile('setup.php', tagRef))?.match(/function plugin_version_([a-z0-9_]+)/)?.[1]; + if (pluginKey !== undefined) { + xmlPath = `${pluginKey}.xml`; + tagXml = await readFile(xmlPath, tagRef); + } + } + if (tagXml === undefined) { + core.info('No plugin XML file found in the tag, skipping update.'); + return; + } + + const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const existingRe = new RegExp(`[ \\t]*\\s*\\s*${escapedTag}\\s*[\\s\\S]*?`); + const block = tagXml.match(existingRe)?.[0]; + if (block === undefined) { + core.info(`No block for ${tag} in the tag's ${xmlPath}, skipping update.`); + return; + } + + const xml = await readFile(xmlPath, base); + if (xml === undefined) { + core.info(`No ${xmlPath} on ${base}, skipping update.`); + return; + } + + let updated; + if (existingRe.test(xml)) { + updated = xml.replace(existingRe, () => block); + } else if (/[ \t]*\r?\n/.test(xml)) { + // Newest first: the block goes right after . + updated = xml.replace(/[ \t]*(\r?\n)/, (match, eol) => `${match}${block}${eol}`); + } else { + core.warning(`No element found in ${xmlPath} on ${base}, skipping update.`); + return; + } + if (updated === xml) { + core.info(`${xmlPath} on ${base} already contains version ${tag}.`); + return; + } + + const branch = `release-xml/${tag}`; + const baseSha = (await github.rest.git.getRef({ owner, repo, ref: `heads/${base}` })).data.object.sha; + try { + await github.rest.git.createRef({ owner, repo, ref: `refs/heads/${branch}`, sha: baseSha }); + } catch (error) { + // 422: branch left by a previous run, reset it onto the default branch. + if (error.status !== 422) { + throw error; + } + await github.rest.git.updateRef({ owner, repo, ref: `heads/${branch}`, sha: baseSha, force: true }); + } + const { data: file } = await github.rest.repos.getContent({ owner, repo, path: xmlPath, ref: branch }); + await github.rest.repos.createOrUpdateFileContents({ + owner, + repo, + path: xmlPath, + branch, + sha: file.sha, + message: `Add version ${tag} to ${xmlPath}`, + content: Buffer.from(updated, 'utf8').toString('base64'), + }); + + const { data: openPulls } = await github.rest.pulls.list({ owner, repo, head: `${owner}:${branch}`, base, state: 'open' }); + if (openPulls.length > 0) { + core.info(`Updated existing PR: ${openPulls[0].html_url}`); + return; + } + const { data: pull } = await github.rest.pulls.create({ + owner, + repo, + head: branch, + base, + title: `Add version ${tag} to ${xmlPath}`, + body: `Copies the \`${tag}\` version block from the tag's \`${xmlPath}\` to \`${base}\`, so that the plugins catalog lists it.`, + }); + core.info(`Opened PR: ${pull.html_url}`); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1332e716..cc67138f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: "Publish release" on: push: tags: - - '*' + - "*" jobs: publish-release: