ci: build on GitHub via the mirror, switchable with BUILD_ON #5

Merged
jcoffey-dev merged 1 commits from ci/build-on-github into main 2026-09-30 06:51:41 +00:00
3 changed files with 132 additions and 35 deletions

No files matched your search

+44 -1
View File
@@ -1,6 +1,14 @@
# CI on the self-hosted Gitea, ported from .gitlab-ci.yml during the move off
# GitLab (2026-09-22). Gitea reads .gitea/workflows and ignores .github/ once
# this directory exists; .github/workflows stays as it was for GitHub.
# this directory exists.
#
# BUILD_ON: an org variable, set on both forges. Gitea push-mirrors this
# repository to GitHub, and with BUILD_ON=github the shellcheck job here skips
# and GitHub Actions runs the same lint on the mirrored copy
# (.github/workflows/ci.yml). The `github` job at the end waits for the result
# that workflow reports back, so this run still passes or fails with the lint.
# Unset, everything runs here as before -- which is also the fallback if
# GitHub is ever unavailable.
#
# Every job runs in an image pinned by digest (tag in the trailing comment),
# and the only action used is coffey-labs/actions/checkout pinned by SHA. The
@@ -19,6 +27,7 @@ concurrency:
jobs:
shellcheck:
if: ${{ vars.BUILD_ON != 'github' }}
runs-on: light
# The -alpine variant, not koalaman/shellcheck:stable. That one is built
# FROM scratch with shellcheck as the entrypoint and no shell at all, so a
@@ -34,3 +43,37 @@ jobs:
[ -n "$files" ] || { echo "no shell scripts found"; exit 1; }
echo "$files" | tr '\n' ' '
shellcheck --severity=warning --format=gcc $files
# ------------------------------------------------------------ github ------
# With BUILD_ON=github the jobs above skip and GitHub Actions builds this
# commit on the push-mirrored copy (.github/workflows/ci.yml). This job waits
# for the commit status that workflow reports back, and passes or fails with
# it, so this run -- what pull requests and merges look at -- still carries
# the real result. If GitHub never reports, it times out and fails: the cue
# to unset BUILD_ON and build here again.
github:
if: ${{ vars.BUILD_ON == 'github' }}
runs-on: light
timeout-minutes: 150
container:
image: node:26-bookworm-slim@sha256:582460f614631b59b824ac6020533b9bf339c7fdf3a6d7db31abb6b4065f0212 # 26-bookworm-slim
env:
READ_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
CONTEXT: ${{ github.ref_type == 'tag' && 'github/ci (tag)' || 'github/ci (branch)' }}
steps:
- run: apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates curl jq >/dev/null
- shell: bash
run: |
set -uo pipefail
url="${CI_SERVER_INTERNAL}/api/v1/repos/${GITHUB_REPOSITORY}/commits/${SHA}/statuses?limit=50"
echo "Waiting for '${CONTEXT}' on ${SHA}"
while :; do
# The newest status for the context is the one that counts.
s="$(curl -fsS -H "Authorization: token ${READ_TOKEN}" "$url" | jq -r --arg c "$CONTEXT" '[.[] | select(.context == $c)] | sort_by(.id) | last | "\(.status) \(.target_url)"' 2>/dev/null)"
case "$s" in
success\ *) echo "GitHub: ${s}"; exit 0 ;;
failure\ *|error\ *) echo "GitHub: ${s}" >&2; exit 1 ;;
esac
sleep 20
done
+88
View File
@@ -0,0 +1,88 @@
# ShellCheck on GitHub Actions, for the GitHub replica of this repository.
# Gitea (git.coffeylabs.org) is the source of truth: it pushes every commit
# here through a push mirror, and this workflow lints what arrives. Pull
# requests live on Gitea, so there is no pull_request trigger -- a PR's branch
# reaches this copy as an ordinary push.
#
# Every file in this repository is a shell script that people are invited to
# run as root, so a lint gate is the cheapest guard against the class of bug
# that keeps showing up here: unquoted expansions, values interpolated into
# commands, and exit statuses that go unchecked.
#
# The switch is the org variable BUILD_ON, set on both forges. With
# BUILD_ON=github every job here runs, the job in .gitea/workflows/ci.yml
# skips, and Gitea's own run waits for the result this workflow reports back.
# With it unset, every job here skips and Gitea lints as before -- which is
# also the way back if GitHub is ever unavailable: unset it on Gitea.
#
# The job is .gitea/workflows/ci.yml's, in the same image pinned by digest,
# with the same settings (severity=warning, gcc format, every script in one
# run). The only action used is actions/checkout, pinned to a full commit SHA
# with the release in a trailing comment.
name: ci
on:
push:
branches: ['**']
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
GITEA_URL: ${{ vars.GITEA_URL }}
jobs:
# Tells Gitea a result is on its way, so the commit does not look unchecked
# while the checks run.
pending:
if: ${{ vars.BUILD_ON == 'github' }}
runs-on: ubuntu-latest
steps:
- env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
CONTEXT: github/ci (${{ github.ref_type }})
run: |
jq -n --arg c "$CONTEXT" --arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
'{state:"pending", context:$c, target_url:$u, description:"GitHub Actions"}' |
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \
--data @- "$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA"
shellcheck:
if: ${{ vars.BUILD_ON == 'github' }}
runs-on: ubuntu-latest
# The -alpine variant, not koalaman/shellcheck:stable. That one is built
# FROM scratch with shellcheck as the entrypoint and no shell at all, so a
# job cannot run a script in it.
container:
image: koalaman/shellcheck-alpine:stable@sha256:c82fe42504fbc9fc68f15d36638e5ee2324ebb8b94e96a3c4e395bf361c49183 # stable
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Reproduces the settings ludeeus/action-shellcheck was given on GitHub:
# severity=warning, gcc format, every script in one run.
- run: |
files=$(find . -name '*.sh' -not -path './.git/*' | sort)
[ -n "$files" ] || { echo "no shell scripts found"; exit 1; }
echo "$files" | tr '\n' ' '
shellcheck --severity=warning --format=gcc $files
# One commit status on Gitea for the whole run. Gitea's ci.yml `github` job
# waits on exactly this context.
report:
if: ${{ always() && vars.BUILD_ON == 'github' }}
needs: [pending, shellcheck]
runs-on: ubuntu-latest
steps:
- env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
CONTEXT: github/ci (${{ github.ref_type }})
STATE: ${{ (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && 'failure' || 'success' }}
run: |
jq -n --arg s "$STATE" --arg c "$CONTEXT" --arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
'{state:$s, context:$c, target_url:$u, description:"GitHub Actions"}' |
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \
--data @- "$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA"
-34
View File
@@ -1,34 +0,0 @@
name: ShellCheck
# Every file in this repository is a shell script that people are
# invited to run as root, so a lint gate is the cheapest guard against
# the class of bug that keeps showing up here: unquoted expansions,
# values interpolated into commands, and exit statuses that go unchecked.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
# warning, not error: the first run at this level surfaced
# exactly two findings and both were fixed, so there is no
# pre-existing backlog to grandfather in. Anything new that
# trips it is genuinely new. Dropping to the default (info)
# would also pull in style suggestions across every script --
# worth doing, but as its own pass.
severity: warning
check_together: 'yes'
format: gcc