Files
Discord-Authenticator/.gitea/workflows/dependency-check.yml
T
DSH Agent 1f4697a885
Build and release image / build (push) Failing after 8s
Serialize image builds across refs
A tag push and a main push can land together and both want to build the same
version and move the same :latest. A per-ref concurrency group let them run in
parallel, which is exactly the race the group is supposed to prevent.
2026-09-20 02:49:40 -04:00

175 lines
7.6 KiB
YAML

name: Check for dependency updates
# Runs every six hours and reports anything out of date in one place: the
# crates in Cargo.lock, the Rust pin in rust-toolchain.toml and the Dockerfile,
# and the Dockerfile's base images.
#
# Everything lands in ONE issue titled "Dependency updates", edited in place
# rather than commented on, so there is a single current answer instead of a
# pile of history. When nothing is out of date the issue is closed — an empty
# issue left open is a thing people stop reading, which defeats the point.
#
# Nothing is upgraded automatically. `cargo update` on a schedule would land
# unreviewed code in a service that holds the operator's second factor, and a
# breaking bump needs a human regardless.
on:
schedule:
# Every six hours, off the hour so this doesn't compete with an image
# rebuild for a runner.
- cron: '23 */6 * * *'
workflow_dispatch:
# One run at a time. A manual dispatch during a scheduled run would otherwise
# race it and could open the issue twice.
concurrency:
group: dependency-check
cancel-in-progress: false
permissions:
contents: read
issues: write
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
# The toolchain comes from the repo rather than being hard-coded here, so
# the check runs on the same compiler the image ships with and this file
# can be copied between repos unchanged.
- name: Resolve the pinned Rust version
id: rust
run: |
set -eu
FILE=deploy/docker/Dockerfile
[ -f "$FILE" ] || FILE=Dockerfile
VERSION=$(grep -E '^ARG RUST_VERSION=' "$FILE" | head -1 | cut -d= -f2)
[ -n "$VERSION" ] || { echo "::error::no ARG RUST_VERSION in $FILE"; exit 1; }
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "checking with rust ${VERSION}"
# The workspace is streamed into the container over stdin rather than
# bind-mounted, so this works whether the runner shares a filesystem with
# the daemon or not. The official rust image already carries the
# toolchain, so nothing is downloaded inside.
#
# RUSTUP_TOOLCHAIN overrides rust-toolchain.toml: the image's own
# toolchain is enough to read Cargo.lock, and honouring the file would
# make every run fetch a second compiler for no benefit.
#
# Cargo is deliberately *not* run offline: at least one project here
# depends on a crate published to a private registry, and `--offline`
# turns that into a failed resolution and therefore no report at all.
- name: Generate report
run: |
set -eu
tar cf - --exclude=./.git --exclude=./target . | docker run --rm -i \
-e RUSTUP_TOOLCHAIN="${{ steps.rust.outputs.version }}" \
-e CARGO_TERM_COLOR=never \
"rust:${{ steps.rust.outputs.version }}-bookworm" \
sh -ec 'mkdir -p /work && cd /work && tar xf - && exec node .gitea/scripts/dependency-report.mjs' \
> /tmp/report.md 2> /tmp/report.err || {
echo "::error::the dependency report failed to generate"
cat /tmp/report.err
exit 1
}
cat /tmp/report.md
grep -E '::dep-check::' /tmp/report.err || true
# An empty report is a broken check, not a clean bill of health.
# Failing here keeps the tracking issue open rather than closing it on
# the strength of a file that never got written.
test -s /tmp/report.md
grep -oE 'routine=[0-9]+ breaking=[0-9]+ images=[0-9]+ rust=[0-9]+' /tmp/report.err \
> /tmp/counts
cat /tmp/counts
- name: Open, update or close the tracking issue
env:
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
REPO: ${{ github.repository }}
run: |
set -eu
API="https://gitea.milesward.dev/api/v1"
node -e '
const fs = require("fs");
const report = fs.readFileSync("/tmp/report.md", "utf8").trim();
const counts = fs.readFileSync("/tmp/counts", "utf8").trim();
const [routine, breaking, images, rust] = counts.match(/[0-9]+/g).map(Number);
const actionable = routine + breaking + images + rust;
const body =
(actionable === 0
? "Everything tracked is current. Closing this until something changes.\n\n"
: "") +
report +
"\n\n---\n\n" +
"Rewritten by `.gitea/workflows/dependency-check.yml` every 6 hours, so this is " +
"always the latest report rather than a pile of comments.";
fs.writeFileSync("/tmp/state.json", JSON.stringify({ actionable, body }));
'
ACTIONABLE=$(node -e 'process.stdout.write(String(require("/tmp/state.json").actionable))')
echo "actionable=$ACTIONABLE"
# Existing issue by title, any state: a previously closed one is
# exactly the one to update and reopen, and searching all states is
# also what stops a duplicate being created after a close.
#
# The `|| true` matters: `set -e` plus `pipefail` would abort the step
# on a transient API error, and creating a second issue is a far worse
# outcome than missing one run.
NUMBER=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/repos/${REPO}/issues?state=all&type=issues&limit=100" \
| node -e '
let s = "";
process.stdin.on("data", d => s += d).on("end", () => {
try {
const issues = JSON.parse(s);
const hit = issues.find(i => i.title === "Dependency updates");
process.stdout.write(hit ? String(hit.number) : "");
} catch { process.stdout.write(""); }
});
' || true)
BODY=$(node -e 'process.stdout.write(JSON.stringify({ body: require("/tmp/state.json").body }))')
if [ -z "$NUMBER" ]; then
if [ "$ACTIONABLE" = "0" ]; then
echo "everything current and no issue open — nothing to do"
exit 0
fi
curl -sf -X POST -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(node -e 'process.stdout.write(JSON.stringify({ title: "Dependency updates", body: require("/tmp/state.json").body }))')" \
"${API}/repos/${REPO}/issues" > /dev/null
echo "opened the tracking issue"
exit 0
fi
curl -sf -X PATCH -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$BODY" "${API}/repos/${REPO}/issues/${NUMBER}" > /dev/null
echo "updated issue #${NUMBER}"
# Reopen when there is work, close when there isn't. Done as a second
# call because the state is not part of the body above.
if [ "$ACTIONABLE" = "0" ]; then WANT=closed; else WANT=open; fi
STATE=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API}/repos/${REPO}/issues/${NUMBER}" \
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).state))')
if [ "$STATE" != "$WANT" ]; then
curl -sf -X PATCH -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"state\":\"${WANT}\"}" "${API}/repos/${REPO}/issues/${NUMBER}" > /dev/null
echo "issue #${NUMBER}: ${STATE} -> ${WANT}"
fi