INBUXA Admin had CI and nothing after it: twelve tags inherited from upstream's numbering, no GitHub releases at all, and no image. Deploying it meant building the tree yourself. This adds the three workflows ihasmail already runs -- weekly release, publish, prune -- and the Dockerfile they need. Monday 09:37 UTC, and nothing on a quiet week. Staggered twenty minutes behind ihasmail-inbuxa's and twenty ahead of the server's, so three releases do not compete for runners and a bad Monday names one repository rather than three. The version is the difference from ihasmail. ihasmail derives its version from the commit it builds, so its release only reads. INBUXA Admin keeps its version in inbuxa-version.json, so the release writes it: the bump is committed to main and the tag names that commit. The tree a tag points at therefore reports the version the tag claims, which a tag placed beside an unbumped file cannot promise. The bump is written with a JSON parser rather than sed, because a version substituted into JSON as a string is one stray quote from a file nothing can read. The image is nginx serving the built files and nothing else -- the interface talks to the mail server from the browser, never from the container. It is built from source in the image rather than copied from dist/, so an image always matches the commit it claims. One image serves any installation: API_BASE_URL writes the `<meta name="api-base-url">` tag that README already documents as the deploy-time way to point the interface at its server. Set nothing and the container still starts, for a build that was given VITE_API_BASE_URL instead. Two things the smoke test found rather than review. Unprivileged nginx runs as uid 101, so the copied files are chowned to it or the tag can never be written. And the directory stays root's, so the entrypoint writes back through the existing file instead of `sed -i`, which replaces the file and needs to create a temp file in the directory. Verified by running it: the tag lands, a deep route falls back to index.html, hashed assets come back immutable while index.html is no-cache, it runs as uid 101, and it starts with no API_BASE_URL set.
183 lines
7.2 KiB
YAML
183 lines
7.2 KiB
YAML
# Cut a release once a week, but only if there is something in it.
|
|
#
|
|
# It does nothing on a quiet week. A release with no commits in it is worse
|
|
# than no release: it moves `:latest` to an identical build, spends a version
|
|
# number, and mails everybody watching the repository about nothing.
|
|
#
|
|
# Unlike ihasmail, whose version is derived from the commit it builds, INBUXA
|
|
# Admin keeps its version in inbuxa-version.json. So this writes it: the bump
|
|
# is committed to main, and the tag names that commit. The commit is the
|
|
# release, which means the tree a tag points at always reports the version the
|
|
# tag claims -- something a tag placed beside an unbumped file cannot promise.
|
|
name: Weekly release
|
|
|
|
on:
|
|
schedule:
|
|
# Mondays, 09:37 UTC -- twenty minutes behind ihasmail-inbuxa's, twenty
|
|
# ahead of the server's. Staggered rather than simultaneous so three
|
|
# releases do not compete for runners, and so a bad Monday names one
|
|
# repository instead of three. GitHub runs scheduled jobs best-effort and
|
|
# can delay a run considerably, so the exact minute is not a promise; the
|
|
# odd minute keeps it off the crowded top of the hour.
|
|
#
|
|
# Note also that GitHub disables scheduled workflows in a repository with
|
|
# no activity for 60 days, which is worth checking for before assuming
|
|
# this file is broken.
|
|
- cron: "37 9 * * 1"
|
|
workflow_dispatch:
|
|
inputs:
|
|
dry_run:
|
|
description: "Work out what would be released, then stop"
|
|
type: boolean
|
|
default: false
|
|
|
|
# One at a time. Two overlapping runs would race to write the same version and
|
|
# create the same tag, and the loser fails noisily for a reason that has
|
|
# nothing to do with the code.
|
|
concurrency:
|
|
group: weekly-release
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
check:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
should_release: ${{ steps.decide.outputs.should_release }}
|
|
version: ${{ steps.decide.outputs.version }}
|
|
tag: ${{ steps.decide.outputs.tag }}
|
|
previous: ${{ steps.decide.outputs.previous }}
|
|
count: ${{ steps.decide.outputs.count }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
ref: main
|
|
fetch-depth: 0
|
|
- id: decide
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# The newest published release, or empty on a repository that has
|
|
# never had one -- in which case everything counts as new. Drafts are
|
|
# excluded: an unpublished draft is not a release anybody has, so
|
|
# counting from it would hide commits that have never shipped.
|
|
previous="$(gh release list --limit 1 --exclude-drafts --json tagName --jq '.[0].tagName // ""')"
|
|
# A tag named by a release is normally present after a full checkout,
|
|
# but a release can outlive its tag. Falling back to the whole
|
|
# history is the safe direction to be wrong in: it over-counts, which
|
|
# cuts a release that was due anyway, where under-counting would skip
|
|
# one that was.
|
|
if [ -n "$previous" ] && git rev-parse -q --verify "refs/tags/${previous}" >/dev/null; then
|
|
count="$(git rev-list --count "${previous}..HEAD")"
|
|
else
|
|
count="$(git rev-list --count HEAD)"
|
|
fi
|
|
|
|
# INBUXA's version is the date, as the rest of the family does it:
|
|
# YYYY.M.D, unpadded. A second release on one day takes a `.N`
|
|
# suffix, counting from 2, which is why this asks the tags rather
|
|
# than assuming today is free.
|
|
today="$(date -u +%Y.%-m.%-d)"
|
|
version="$today"
|
|
n=2
|
|
while git rev-parse -q --verify "refs/tags/v${version}" >/dev/null; do
|
|
version="${today}.${n}"
|
|
n=$((n + 1))
|
|
done
|
|
|
|
should_release=true
|
|
reason=""
|
|
if [ "$count" -eq 0 ]; then
|
|
should_release=false
|
|
reason="no commits since ${previous}"
|
|
fi
|
|
|
|
{
|
|
echo "should_release=$should_release"
|
|
echo "version=$version"
|
|
echo "tag=v${version}"
|
|
echo "previous=$previous"
|
|
echo "count=$count"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
# Written to the run summary so a skipped week reads as a decision
|
|
# rather than as a workflow that quietly did nothing.
|
|
{
|
|
echo "### Weekly release"
|
|
echo
|
|
if [ "$should_release" = "true" ]; then
|
|
echo "Releasing **v${version}** — ${count} commit(s) since ${previous:-the beginning}."
|
|
else
|
|
echo "Nothing to release: ${reason}."
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
cut:
|
|
needs: check
|
|
if: needs.check.outputs.should_release == 'true' && !inputs.dry_run
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
outputs:
|
|
sha: ${{ steps.bump.outputs.sha }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
ref: main
|
|
fetch-depth: 0
|
|
- id: bump
|
|
env:
|
|
VERSION: ${{ needs.check.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Rewritten with a JSON parser rather than sed: the file is small and
|
|
# the shape is known, but a version written into JSON by string
|
|
# substitution is one stray quote away from a file nothing can read.
|
|
node -e '
|
|
const fs = require("fs");
|
|
const f = "inbuxa-version.json";
|
|
const j = JSON.parse(fs.readFileSync(f, "utf8"));
|
|
j.version = process.env.VERSION;
|
|
fs.writeFileSync(f, JSON.stringify(j, null, 2) + "\n");
|
|
'
|
|
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git add inbuxa-version.json
|
|
git commit -m "Version ${VERSION}"
|
|
git push origin HEAD:main
|
|
|
|
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
- env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
args=(--target "${{ steps.bump.outputs.sha }}"
|
|
--title "INBUXA Admin ${{ needs.check.outputs.version }}"
|
|
--generate-notes)
|
|
# Bound the notes to what is actually new. Without a start tag the
|
|
# generator reaches back to whatever it decides is previous, which on
|
|
# a repository carrying older tag shapes -- this one still has the
|
|
# inherited v1.0.x tags -- is not always the last release.
|
|
if [ -n "${{ needs.check.outputs.previous }}" ]; then
|
|
args+=(--notes-start-tag "${{ needs.check.outputs.previous }}")
|
|
fi
|
|
gh release create "${{ needs.check.outputs.tag }}" "${args[@]}"
|
|
|
|
# Called rather than left to the `release` trigger on purpose: see the note
|
|
# at the top of publish.yml. A release created with GITHUB_TOKEN raises no
|
|
# event, so without this the tag would exist and no image would follow it.
|
|
publish:
|
|
needs: [check, cut]
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
uses: ./.github/workflows/publish.yml
|
|
with:
|
|
ref: ${{ needs.cut.outputs.sha }}
|
|
tag_latest: true
|