Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/label-commenter-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
8 changes: 7 additions & 1 deletion .github/workflows/auto-tag-new-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
41 changes: 21 additions & 20 deletions .github/workflows/close_stale_issue.yml
Original file line number Diff line number Diff line change
@@ -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/fields'
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.
5 changes: 5 additions & 0 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
106 changes: 106 additions & 0 deletions .github/workflows/forward-port.yml
Original file line number Diff line number Diff line change
@@ -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 `<version>/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 `[<branch>]` 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"
}
7 changes: 4 additions & 3 deletions .github/workflows/label-commenter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
issues:
types:
- "labeled"
- "unlabeled"

jobs:
comment:
Expand All @@ -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
157 changes: 157 additions & 0 deletions .github/workflows/open-xml-update-pr.yml
Original file line number Diff line number Diff line change
@@ -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/fields' && 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]*<version>\\s*<num>\\s*${escapedTag}\\s*</num>[\\s\\S]*?</version>`);
const block = tagXml.match(existingRe)?.[0];
if (block === undefined) {
core.info(`No <version> 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 (/<versions>[ \t]*\r?\n/.test(xml)) {
// Newest first: the block goes right after <versions>.
updated = xml.replace(/<versions>[ \t]*(\r?\n)/, (match, eol) => `${match}${block}${eol}`);
} else {
core.warning(`No <versions> 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}`);
Loading
Loading