Add option output_action (#178)

This commit is contained in:
Simon Lecoq
2021-03-08 23:38:40 +01:00
committed by GitHub
parent 64fe0c8ef0
commit e0bfbf62fd
5 changed files with 145 additions and 29 deletions

View File

@@ -182,12 +182,11 @@ inputs:
description: Use mocked data instead of live APIs description: Use mocked data instead of live APIs
default: no default: no
# Use a pre-built image from GitHub registry when using unreleased versions of "lowlighter/metrics" # Use a pre-built image from GitHub registry (experimental)
# This option has no effect on forks (images will always be rebuilt from Dockerfile)
# See https://github.com/users/lowlighter/packages/container/package/metrics for more information # See https://github.com/users/lowlighter/packages/container/package/metrics for more information
use_prebuilt_image: use_prebuilt_image:
description: Use pre-built image from GitHub registry description: Use pre-built image from GitHub registry
default: yes default: ""
# ==================================================================================== # ====================================================================================
# 📰 Recent activity # 📰 Recent activity
@@ -847,7 +846,6 @@ runs:
steps: steps:
- run: | - run: |
# Create environment file from inputs and GitHub variables # Create environment file from inputs and GitHub variables
echo "::group::Metrics docker image setup"
cd $METRICS_ACTION_PATH cd $METRICS_ACTION_PATH
touch .env touch .env
for INPUT in $(echo $INPUTS | jq -r 'to_entries|map("INPUT_\(.key|ascii_upcase)=\(.value|@uri)")|.[]'); do for INPUT in $(echo $INPUTS | jq -r 'to_entries|map("INPUT_\(.key|ascii_upcase)=\(.value|@uri)")|.[]'); do
@@ -866,11 +864,20 @@ runs:
# Image tag (extracted from version or from env) # Image tag (extracted from version or from env)
METRICS_TAG=v$(echo $METRICS_VERSION | sed -r 's/^([0-9]+[.][0-9]+).*/\1/') METRICS_TAG=v$(echo $METRICS_VERSION | sed -r 's/^([0-9]+[.][0-9]+).*/\1/')
if [[ $METRICS_USE_PREBUILT_IMAGE ]]; then
METRICS_TAG=$METRICS_USE_PREBUILT_IMAGE
echo "Pre-built image: yes"
fi
echo "Image tag: $METRICS_TAG" echo "Image tag: $METRICS_TAG"
# Image name # Image name
# Pre-built image
if [[ $METRICS_USE_PREBUILT_IMAGE ]]; then
echo "Using pre-built version $METRICS_TAG, will pull docker image from GitHub registry"
METRICS_IMAGE=ghcr.io/lowlighter/metrics:$METRICS_TAG
docker image pull $METRICS_IMAGE > /dev/null
# Official action # Official action
if [[ $METRICS_SOURCE == "lowlighter" ]]; then elif [[ $METRICS_SOURCE == "lowlighter" ]]; then
# Is released version # Is released version
set +e set +e
METRICS_IS_RELEASED=$(expr $(expr match $METRICS_VERSION .*-beta) == 0) METRICS_IS_RELEASED=$(expr $(expr match $METRICS_VERSION .*-beta) == 0)
@@ -880,14 +887,7 @@ runs:
if [[ "$METRICS_IS_RELEASED" -gt "0" ]]; then if [[ "$METRICS_IS_RELEASED" -gt "0" ]]; then
echo "Using released version $METRICS_TAG, will pull docker image from GitHub registry" echo "Using released version $METRICS_TAG, will pull docker image from GitHub registry"
METRICS_IMAGE=ghcr.io/lowlighter/metrics:$METRICS_TAG METRICS_IMAGE=ghcr.io/lowlighter/metrics:$METRICS_TAG
docker image pull $METRICS_IMAGE docker image pull $METRICS_IMAGE > /dev/null
# Use registry for unreleased version with pre-built images
elif [[ ! $METRICS_USE_PREBUILT_IMAGE =~ ^([Ff]alse|[Oo]ff|[Nn]o|0)$ ]]; then
METRICS_TAG="$METRICS_TAG-beta"
echo "Image tag (updated): $METRICS_TAG"
echo "Using pre-built version $METRICS_TAG, will pull docker image from GitHub registry"
METRICS_IMAGE=ghcr.io/lowlighter/metrics:$METRICS_TAG
docker image pull $METRICS_IMAGE
# Rebuild image for unreleased version # Rebuild image for unreleased version
else else
echo "Using an unreleased version ($METRICS_VERSION)" echo "Using an unreleased version ($METRICS_VERSION)"
@@ -902,16 +902,15 @@ runs:
# Build image if necessary # Build image if necessary
set +e set +e
docker image inspect $METRICS_IMAGE docker image inspect $METRICS_IMAGE > /dev/null
METRICS_IMAGE_NEEDS_BUILD="$?" METRICS_IMAGE_NEEDS_BUILD="$?"
set -e set -e
if [[ "$METRICS_IMAGE_NEEDS_BUILD" -gt "0" ]]; then if [[ "$METRICS_IMAGE_NEEDS_BUILD" -gt "0" ]]; then
echo "Image $METRICS_IMAGE is not present locally, rebuilding it from Dockerfile" echo "Image $METRICS_IMAGE is not present locally, rebuilding it from Dockerfile"
docker build -t $METRICS_IMAGE . docker build -t $METRICS_IMAGE . > /dev/null
else else
echo "Image $METRICS_IMAGE is present locally" echo "Image $METRICS_IMAGE is present locally"
fi fi
echo "::endgroup::"
# Run docker image with current environment # Run docker image with current environment
docker run --init --volume $GITHUB_EVENT_PATH:$GITHUB_EVENT_PATH --env-file .env $METRICS_IMAGE docker run --init --volume $GITHUB_EVENT_PATH:$GITHUB_EVENT_PATH --env-file .env $METRICS_IMAGE

View File

@@ -34,7 +34,12 @@ runs:
echo $INPUT >> .env echo $INPUT >> .env
done done
env | grep -E '^(GITHUB|ACTIONS|CI)' >> .env env | grep -E '^(GITHUB|ACTIONS|CI)' >> .env
echo "Environment variable: loaded" echo "Environment variables: loaded"
# Renders output folder
METRICS_RENDERS="/metrics_renders"
sudo mkdir -p $METRICS_RENDERS
echo "Renders output folder: $METRICS_RENDERS"
# Source repository (picked from action name) # Source repository (picked from action name)
METRICS_SOURCE=$(echo $METRICS_ACTION | sed -E 's/metrics.*?$//g') METRICS_SOURCE=$(echo $METRICS_ACTION | sed -E 's/metrics.*?$//g')
@@ -94,7 +99,7 @@ runs:
echo "::endgroup::" echo "::endgroup::"
# Run docker image with current environment # Run docker image with current environment
docker run --init --volume $GITHUB_EVENT_PATH:$GITHUB_EVENT_PATH --env-file .env $METRICS_IMAGE docker run --init --volume $GITHUB_EVENT_PATH:$GITHUB_EVENT_PATH --volume $METRICS_RENDERS:/renders --env-file .env $METRICS_IMAGE
rm .env rm .env
shell: bash shell: bash
env: env:

View File

@@ -5,6 +5,8 @@
import setup from "../metrics/setup.mjs" import setup from "../metrics/setup.mjs"
import mocks from "../mocks/index.mjs" import mocks from "../mocks/index.mjs"
import metrics from "../metrics/index.mjs" import metrics from "../metrics/index.mjs"
import fs from "fs/promises"
import paths from "path"
process.on("unhandledRejection", error => { throw error }) //eslint-disable-line max-statements-per-line, brace-style process.on("unhandledRejection", error => { throw error }) //eslint-disable-line max-statements-per-line, brace-style
//Debug message buffer //Debug message buffer
@@ -40,6 +42,10 @@
console.log("Skipped because [Skip GitHub Action] is in commit message") console.log("Skipped because [Skip GitHub Action] is in commit message")
process.exit(0) process.exit(0)
} }
if (/Auto-generated metrics for run #\d+/.test(github.context.payload.head_commit.message)) {
console.log("Skipped because this seems to be an automated pull request merge")
process.exit(0)
}
} }
//Load configuration //Load configuration
@@ -58,6 +64,7 @@
"committer.token":_token, "committer.branch":_branch, "committer.token":_token, "committer.branch":_branch,
"use.prebuilt.image":_image, "use.prebuilt.image":_image,
retries, "retries.delay":retries_delay, retries, "retries.delay":retries_delay,
"output.action":_action,
...config ...config
} = metadata.plugins.core.inputs.action({core}) } = metadata.plugins.core.inputs.action({core})
const q = {...query, ...(_repo ? {repo:_repo} : null), template} const q = {...query, ...(_repo ? {repo:_repo} : null), template}
@@ -73,6 +80,7 @@
DEBUG = false DEBUG = false
} }
info("Debug flags", dflags) info("Debug flags", dflags)
q["debug.flags"] = dflags.join(" ")
//Token for data gathering //Token for data gathering
info("GitHub token", token, {token:true}) info("GitHub token", token, {token:true})
@@ -112,13 +120,17 @@
const committer = {} const committer = {}
if (!dryrun) { if (!dryrun) {
//Compute committer informations //Compute committer informations
committer.commit = true
committer.token = _token || token committer.token = _token || token
committer.commit = true
committer.pr = /^pull-request/.test(_action)
committer.merge = _action.match(/^pull-request-(?<method>merge|squash|rebase)$/)?.groups?.method ?? null
committer.branch = _branch || github.context.ref.replace(/^refs[/]heads[/]/, "") committer.branch = _branch || github.context.ref.replace(/^refs[/]heads[/]/, "")
committer.head = committer.pr ? `metrics-run-${github.context.runId}` : committer.branch
info("Committer token", committer.token, {token:true}) info("Committer token", committer.token, {token:true})
if (!committer.token) if (!committer.token)
throw new Error("You must provide a valid GitHub token to commit your metrics") throw new Error("You must provide a valid GitHub token to commit your metrics")
info("Committer branch", committer.branch) info("Committer branch", committer.branch)
info("Committer head branch", committer.head)
//Instantiate API for committer //Instantiate API for committer
committer.rest = github.getOctokit(committer.token) committer.rest = github.getOctokit(committer.token)
info("Committer REST API", "ok") info("Committer REST API", "ok")
@@ -128,13 +140,29 @@
catch { catch {
info("Committer account", "(github-actions)") info("Committer account", "(github-actions)")
} }
//Create head branch if needed
try {
await committer.rest.git.getRef({...github.context.repo, ref:`heads/${committer.head}`})
info("Committer head branch status", "ok")
}
catch (error) {
console.debug(error)
if (/not found/i.test(`${error}`)) {
const {data:{object:{sha}}} = await committer.rest.git.getRef({...github.context.repo, ref:`heads/${committer.branch}`})
info("Committer branch current sha", sha)
await committer.rest.git.createRef({...github.context.repo, ref:`refs/heads/${committer.head}`, sha})
info("Committer head branch status", "(created)")
}
else
throw error
}
//Retrieve previous render SHA to be able to update file content through API //Retrieve previous render SHA to be able to update file content through API
committer.sha = null committer.sha = null
try { try {
const {repository:{object:{oid}}} = await graphql(` const {repository:{object:{oid}}} = await graphql(`
query Sha { query Sha {
repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") { repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
object(expression: "${committer.branch}:${filename}") { ... on Blob { oid } } object(expression: "${committer.head}:${filename}") { ... on Blob { oid } }
} }
} }
`, {headers:{authorization:`token ${committer.token}`}}) `, {headers:{authorization:`token ${committer.token}`}})
@@ -204,7 +232,7 @@
info.break() info.break()
info.section("Rendering") info.section("Rendering")
let error = null, rendered = null let error = null, rendered = null
for (let attempt = 0; attempt < retries; attempt++) { for (let attempt = 1; attempt <= retries; attempt++) {
try { try {
console.debug(`::group::Attempt ${attempt}/${retries}`) console.debug(`::group::Attempt ${attempt}/${retries}`)
;({rendered} = await metrics({login:user, q}, {graphql, rest, plugins, conf, die, verify, convert}, {Plugins, Templates})) ;({rendered} = await metrics({login:user, q}, {graphql, rest, plugins, conf, die, verify, convert}, {Plugins, Templates}))
@@ -222,15 +250,53 @@
throw error ?? new Error("Could not render metrics") throw error ?? new Error("Could not render metrics")
info("Status", "complete") info("Status", "complete")
//Save output to renders output folder
info.break()
info.section("Saving")
await fs.writeFile(paths.join("/renders", filename), Buffer.from(rendered))
info(`Save to /metrics_renders/${filename}`, "ok")
//Commit metrics //Commit metrics
if (committer.commit) { if (committer.commit) {
await committer.rest.repos.createOrUpdateFileContents({ await committer.rest.repos.createOrUpdateFileContents({
...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`, ...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`,
content:Buffer.from(rendered).toString("base64"), content:Buffer.from(rendered).toString("base64"),
branch:committer.branch, branch:committer.pr ? committer.head : committer.branch,
...(committer.sha ? {sha:committer.sha} : {}), ...(committer.sha ? {sha:committer.sha} : {}),
}) })
info("Commit to repository", "success") info(`Commit to branch ${committer.branch}`, "ok")
}
//Pull request
if (committer.pr) {
//Create pull request
let number = null
try {
({data:{number}} = await committer.rest.pulls.create({...github.context.repo, head:committer.head, base:committer.branch, title:`Auto-generated metrics for run #${github.context.runId}`, body:" ", maintainer_can_modify:true}))
info(`Pull request from ${committer.head} to ${committer.branch}`, "(created)")
}
catch (error) {
console.debug(error)
if (/A pull request already exists/.test(error)) {
info(`Pull request from ${committer.head} to ${committer.branch}`, "(already existing)")
const q = `repo:${github.context.repo.owner}/${github.context.repo.repo}+type:pr+state:open+Auto-generated metrics for run #${github.context.runId}+in:title`
const prs = (await committer.rest.search.issuesAndPullRequests({q})).data.items.filter(({user:{login}}) => login === "github-actions[bot]")
if (prs.length < 1)
throw new Error("0 matching prs. Cannot preoceed.")
if (prs.length > 1)
throw new Error(`Found more than one matching prs: ${prs.map(({number}) => `#${number}`).join(", ")}. Cannot proceed.`)
;({number} = prs.shift())
}
else
throw error
}
info("Pull request number", number)
//Merge pull request
if (committer.merge) {
info("Merge method", committer.merge)
await committer.rest.pulls.merge({...github.context.repo, pull_number:number, merge_method:committer.merge})
info(`Merge #${number} to ${committer.branch}`, "ok")
}
} }
//Success //Success

View File

@@ -102,6 +102,38 @@ Specify a single value to apply it to both height and with, and two values to us
config_padding: 6%, 10% # 6% width padding, 10% height padding config_padding: 6%, 10% # 6% width padding, 10% height padding
``` ```
### 🧶 Using commits, pull requests or manual review to handle metrics output
It is possible to configure output behaviour using `output_action` option, which can be set to:
- `none`, where output will be generated in `/rendered/${filename}` without being pushed
- You can then manually post-process it
- `commit` (default), where output will directly be committed and pushed to `committer_branch`
- `pull-request`, where output will be committed to a new branch with current run id waiting for to be merged in `committer_branch`
- By appending either `-merge`, `-squash` or `-rebase`, pull request will be automatically merged with given method
- This method is useful to combine all editions of a single run with multiples metrics steps into a single commit on targetted branch
- If you choose to manually merge pull requests, be sure to disable `push:` triggers on your workflow, as it'll count as your own commit
#### Examples workflows
```yaml
# The following will:
# - open a pull request with "my-metrics-0.svg" as first commit
# - append "my-metrics-1.svg" as second commit
# - merge pull request (as second step is set to "pull-request-merge")
- uses: lowlighter/metrics@latest
with:
# ... other options
filename: my-metrics-0.svg
output_action: pull-request
- uses: lowlighter/metrics@latest
with:
# ... other options
filename: my-metrics-1.svg
output_action: pull-request-merge
```
### ♻️ Retrying automatically failed rendering ### ♻️ Retrying automatically failed rendering
Rendering is subject to external factors and can fail from time to time. Rendering is subject to external factors and can fail from time to time.

View File

@@ -51,6 +51,19 @@ inputs:
type: string type: string
default: github-metrics.svg default: github-metrics.svg
# Output action
output_action:
description: Output action
type: string
default: commit
values:
- none # Only generate file in "/metrics_renders"
- commit # Commit output to "committer_branch"
- pull-request # Commit output to a new branch and open a pull request to "committer_branch"
- pull-request-merge # Same as "pull-request" and additionaly merge pull request
- pull-request-squash # Same as "pull-request" and additionaly squash and merge pull request
- pull-request-rebase # Same as "pull-request" and additionaly rebase and merge pull request
# Optimize SVG image to reduce its filesize # Optimize SVG image to reduce its filesize
# Some templates may not support this option # Some templates may not support this option
optimize: optimize:
@@ -189,7 +202,8 @@ inputs:
- --halloween - --halloween
- --error - --error
# Dry-run mode (perform generation without pushing it) # Dry-run mode (perform generation without output)
# Unlike "output_action" set to "none", output file won't be available in "/metrics_renders"
dryrun: dryrun:
description: Enable dry-run description: Enable dry-run
type: boolean type: boolean