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"
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/mreporting' && 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}`);
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: "Publish release"
on:
push:
tags:
- '*'
- "*"

jobs:
publish-release:
Expand Down
Loading