ci: build on GitHub via the mirror, switchable with BUILD_ON #8
No files matched your search
+47
-3
@@ -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 build jobs here skip and
|
||||
# GitHub Actions does the same work on the mirrored copy
|
||||
# (.github/workflows/ci.yml, on faster and native-arm64 runners). The `github`
|
||||
# job at the end waits for the result that workflow reports back, so this run
|
||||
# still passes or fails with the build. Unset, everything builds here as
|
||||
# before -- which is also the fallback if GitHub is ever unavailable.
|
||||
#
|
||||
# This one mattered more than most. ghcr.io/coffey-labs/ihasvpn went dark with
|
||||
# the account while the deployment was still pulling from it, and the only
|
||||
@@ -37,6 +45,7 @@ jobs:
|
||||
# against the placeholder too.) The image build does its own frontend build
|
||||
# in the Dockerfile.
|
||||
web:
|
||||
if: ${{ vars.BUILD_ON != 'github' }}
|
||||
runs-on: light
|
||||
container:
|
||||
image: node:26-bookworm-slim@sha256:582460f614631b59b824ac6020533b9bf339c7fdf3a6d7db31abb6b4065f0212 # 26-bookworm-slim
|
||||
@@ -50,6 +59,7 @@ jobs:
|
||||
npm run build
|
||||
|
||||
go:
|
||||
if: ${{ vars.BUILD_ON != 'github' }}
|
||||
needs: [web]
|
||||
runs-on: light
|
||||
container:
|
||||
@@ -63,7 +73,7 @@ jobs:
|
||||
- run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||
|
||||
docker-build:
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
if: ${{ vars.BUILD_ON != 'github' && (!startsWith(github.ref, 'refs/tags/')) }}
|
||||
needs: [web]
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -95,7 +105,7 @@ jobs:
|
||||
# The push logs in with PACKAGE_TOKEN (jcoffey-dev, write:package): Gitea's
|
||||
# per-job token is refused by the container registry.
|
||||
publish:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
if: ${{ vars.BUILD_ON != 'github' && (startsWith(github.ref, 'refs/tags/')) }}
|
||||
needs: [web, go]
|
||||
runs-on: docker
|
||||
container:
|
||||
@@ -133,3 +143,37 @@ jobs:
|
||||
|| echo "package already linked (or link refused); not fatal"
|
||||
- if: always()
|
||||
run: docker logout "$REGISTRY" || true
|
||||
|
||||
# ------------------------------------------------------------ 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
|
||||
+194
-17
@@ -1,33 +1,210 @@
|
||||
name: CI
|
||||
# CI and image publishing on GitHub Actions, for the GitHub replica of this
|
||||
# repository. Gitea (git.coffeylabs.org) is the source of truth: it pushes
|
||||
# every commit and tag here through a push mirror, and this workflow builds
|
||||
# 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.
|
||||
#
|
||||
# The switch is the org variable BUILD_ON, set on both forges. With
|
||||
# BUILD_ON=github every job here runs, the heavy jobs in .gitea/workflows/ci.yml
|
||||
# skip, and Gitea's own run waits for the result this workflow reports back.
|
||||
# With it unset, every job here skips and Gitea builds everything as before --
|
||||
# which is also the way back if GitHub is ever unavailable: unset it on Gitea.
|
||||
#
|
||||
# This file does what .gitea/workflows/ci.yml does, job for job; that file is
|
||||
# the spec for what CI covers. The difference is the image publish: here each
|
||||
# architecture builds on its own native runner (`ubuntu-24.04-arm` is free for
|
||||
# public repositories) instead of arm64 under QEMU, and the two digests are
|
||||
# joined into one tag.
|
||||
#
|
||||
# Images go to the Gitea registry (vars.REGISTRY), as they do from Gitea, not
|
||||
# to GHCR. Credentials: the org secret GITEA_TOKEN, a jcoffey-dev Gitea token
|
||||
# with write:repository and write:package -- the registry push, the package
|
||||
# link, and the commit status reported back to Gitea.
|
||||
#
|
||||
# Every `uses:` is pinned to a full commit SHA with the release in a trailing
|
||||
# comment. Do not "simplify" one back to a tag.
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
# Lets a run be started by hand against any ref, including one GitHub
|
||||
# queued and then orphaned.
|
||||
branches: ['**']
|
||||
tags: ['**']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL }}
|
||||
REGISTRY: ${{ vars.REGISTRY }}
|
||||
|
||||
jobs:
|
||||
check:
|
||||
# Tells Gitea a result is on its way, so the commit does not look unchecked
|
||||
# while the build runs.
|
||||
pending:
|
||||
if: ${{ vars.BUILD_ON == 'github' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v7
|
||||
- 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"
|
||||
|
||||
# The Go binary embeds the built web assets, so the frontend build comes
|
||||
# first. As on Gitea, its output is not handed on: the committed
|
||||
# dist/.gitkeep is enough for the embed to compile, and the image build does
|
||||
# its own frontend build in the Dockerfile.
|
||||
web:
|
||||
if: ${{ vars.BUILD_ON == 'github' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 26
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: npm ci --ignore-scripts --no-audit --no-fund
|
||||
working-directory: web
|
||||
- run: npm run build
|
||||
working-directory: web
|
||||
- uses: actions/setup-go@v6
|
||||
- working-directory: web
|
||||
run: |
|
||||
npm ci --ignore-scripts --no-audit --no-fund
|
||||
npm run build
|
||||
|
||||
go:
|
||||
if: ${{ vars.BUILD_ON == 'github' }}
|
||||
needs: [web]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: go vet ./...
|
||||
- run: go test -count=1 ./...
|
||||
- name: govulncheck
|
||||
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||
- name: Docker build
|
||||
run: docker build -t ihasvpn:ci .
|
||||
# Unpinned on purpose, as on Gitea: a vulnerability check wants today's
|
||||
# database, not a pinned copy of last month's.
|
||||
- run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||
|
||||
# Proves the Dockerfile still builds, without pushing. Branches only; a tag
|
||||
# builds the real image below.
|
||||
docker-build:
|
||||
if: ${{ vars.BUILD_ON == 'github' && github.ref_type != 'tag' }}
|
||||
needs: [web]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- run: docker build -t "ihasvpn:ci-${GITHUB_SHA::8}" .
|
||||
|
||||
# ----------------------------------------------------------- publish ------
|
||||
# The version is the tag without its leading "v", the same string the Gitea
|
||||
# publish job builds with, so /api/health reports the same thing whichever
|
||||
# forge built the image.
|
||||
build:
|
||||
if: ${{ vars.BUILD_ON == 'github' && github.ref_type == 'tag' }}
|
||||
needs: [web, go]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- run: |
|
||||
echo "IMAGE=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
|
||||
echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
|
||||
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ${{ vars.REGISTRY }}
|
||||
username: jcoffey-dev
|
||||
password: ${{ secrets.GITEA_TOKEN }}
|
||||
- name: Build and push by digest
|
||||
id: push
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: IHASVPN_VERSION=${{ env.VERSION }}
|
||||
# Attestations would add manifests of their own to the index, and
|
||||
# `imagetools create` below expects just the two it pushed.
|
||||
provenance: false
|
||||
sbom: false
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
- name: Save the digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.push.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digest-${{ strategy.job-index }}
|
||||
path: /tmp/digests/*
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
# Joins the per-architecture digests into :<version> and :latest -- every
|
||||
# tag moves :latest, as it does on Gitea -- then links the package to the
|
||||
# repository so it shows on its Packages tab (idempotent).
|
||||
publish:
|
||||
if: ${{ vars.BUILD_ON == 'github' && github.ref_type == 'tag' }}
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digest-*
|
||||
merge-multiple: true
|
||||
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ${{ vars.REGISTRY }}
|
||||
username: jcoffey-dev
|
||||
password: ${{ secrets.GITEA_TOKEN }}
|
||||
- name: Create the manifest
|
||||
run: |
|
||||
IMAGE="${REGISTRY}/${GITHUB_REPOSITORY,,}"
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
refs=()
|
||||
for f in /tmp/digests/*; do
|
||||
refs+=("${IMAGE}@sha256:$(basename "$f")")
|
||||
done
|
||||
docker buildx imagetools create -t "${IMAGE}:${VERSION}" -t "${IMAGE}:latest" "${refs[@]}"
|
||||
docker buildx imagetools inspect "${IMAGE}:${VERSION}"
|
||||
- env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
owner="${GITHUB_REPOSITORY%%/*}"; name="${GITHUB_REPOSITORY#*/}"
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA_URL/api/v1/packages/${owner,,}/container/${name,,}/-/link/${name}" \
|
||||
|| echo "package already linked (or link refused); not fatal"
|
||||
|
||||
# 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, web, go, docker-build, build, publish]
|
||||
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"
|
||||
@@ -1,127 +0,0 @@
|
||||
# Publish the container image to GHCR.
|
||||
#
|
||||
# FIRST RUN: a package GHCR creates for the first time is private, even in a
|
||||
# public repository. Set it to public by hand under the package's settings
|
||||
# and check with a logged-out `docker pull`.
|
||||
#
|
||||
# Two architectures, each built on its own native runner rather than under
|
||||
# QEMU (`ubuntu-24.04-arm` is free for public repositories). Each runner
|
||||
# pushes an untagged image by digest and a final job joins the two into one
|
||||
# multi-arch tag.
|
||||
name: Publish image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Tag, branch or SHA to build"
|
||||
required: true
|
||||
default: main
|
||||
tag_latest:
|
||||
description: "Also move :latest to this build"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
IMAGE: ghcr.io/coffey-labs/ihasvpn
|
||||
|
||||
jobs:
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.v.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
fetch-depth: 0
|
||||
- id: v
|
||||
run: |
|
||||
V="$(git describe --tags --always --dirty)"
|
||||
V="${V#v}"
|
||||
echo "version=$V" >> "$GITHUB_OUTPUT"
|
||||
echo "version $V"
|
||||
|
||||
build:
|
||||
needs: version
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push by digest
|
||||
id: push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: IHASVPN_VERSION=${{ needs.version.outputs.version }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
- name: Save the digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.push.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digest-${{ strategy.job-index }}
|
||||
path: /tmp/digests/*
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
needs: [version, build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digest-*
|
||||
merge-multiple: true
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create the manifest
|
||||
run: |
|
||||
tags=(-t "${IMAGE}:${{ needs.version.outputs.version }}")
|
||||
if [ "${{ github.event_name }}" = "release" ] && [ "${{ github.event.release.prerelease }}" = "false" ]; then
|
||||
tags+=(-t "${IMAGE}:latest")
|
||||
elif [ "${{ inputs.tag_latest }}" = "true" ]; then
|
||||
tags+=(-t "${IMAGE}:latest")
|
||||
fi
|
||||
refs=()
|
||||
for f in /tmp/digests/*; do
|
||||
refs+=("${IMAGE}@sha256:$(basename "$f")")
|
||||
done
|
||||
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
||||
- name: Show what landed
|
||||
run: docker buildx imagetools inspect "${IMAGE}:${{ needs.version.outputs.version }}"
|
||||
Reference in new issue
Block a user