Files
james.maesandClaude Opus 5.5 27192881a6 windows, macos: release with the job token
The org PACKAGES_TOKEN got HTTP 403 on the release lookup; go.yaml and
gradle.yaml already use the job token (contents: write). Do the same here
and drop the now-unused PACKAGES_TOKEN secret input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:19:00 -06:00

14 KiB

ci-workflows

Shared Gitea Actions workflows for KOF and QRun repositories. A small, direct port of the CircleCI qqq-orb. Keep it that way.

Maven

Add one file to the repository, .gitea/workflows/build.yaml:

name: build
on:
  push:
jobs:
  maven:
    uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
    secrets: inherit
    # with:
    #   java-version: '17'
    #   maven-args: -Pui-demo

The repository needs a <revision> property in the root pom.xml.

Every push builds once. PRs use the push run's status on the head commit. Builds run on the container-build runners, which provide a Docker daemon for Testcontainers.

Ref Version Action
feature and other branches, main unchanged mvn verify
develop next minor -SNAPSHOT after a release, else unchanged test, deploy, commit bump
release/X.Y X.Y.0-RC.N test, deploy, commit bump, tag vX.Y.0-RC.N
hotfix/* patch + 1 test, deploy, commit bump, tag vX.Y.Z
tag vX.Y.Z X.Y.Z test, deploy, commit to main
tag publish-* X.Y.Z-<feature>-<sha>-SNAPSHOT test, deploy

Packages go to the owner's Gitea Maven registry, https://git.kof22.com/api/packages/<owner>/maven, using the job token (or an optional PACKAGES_TOKEN secret). Builds resolve dependencies from the QRun-IO registry as well as Maven Central.

Node

.gitea/workflows/build.yaml:

name: build
on:
  push:
jobs:
  node:
    uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/node.yaml@v1
    with:
      node-version: '24'
      commands: |
        npm run lint
        npm test
Input Default Meaning
commands required Shell commands, one per line, run in order; the first failure fails the build
node-version '22' Selects the official node:<version>-bookworm image
install npm ci Dependency install command
uv false Install a pinned, sha256-verified uv for Python script checks
working-directory . Where the install and commands run
runner container-build Runner label
publish false On a release tag, publish after every command passed (below)
version-file package.json The release tag must be v<version> of this file, or the build fails
npm-publish-dirs . Package directories to npm publish, one per line, in order; empty = none
publish-commands empty Commands that publish instead of the npm-publish-dirs loop
release-files empty Files or globs, one per line, for a Gitea release; empty = no release

Publishing

Without publish: true the workflow only verifies. With it, branches still only verify; a tag vX.Y.Z or vX.Y.Z-<pre> (for example v1.2.0-RC.1) publishes:

  1. The tag must equal v + the version-file version. Set the version in package.json (npm version 1.2.0-RC.1 --no-git-tag-version), merge, then tag the merge commit. A mismatch fails the build before anything is installed.
  2. Install and commands run as on any push; a failure stops the release.
  3. Each npm-publish-dirs package goes to the owner's Gitea npm registry, https://git.kof22.com/api/packages/<owner>/npm/, with the PACKAGES_TOKEN org secret (secrets: inherit). A prerelease version gets dist-tag next, a final version latest. The package keeps its name and scope; a publishConfig.registry (npmjs, GitHub) is ignored for the publish. A version that already exists fails: publish a new version instead.
  4. release-files become a Gitea release for the tag (a prerelease for -<pre> versions) with a fresh SHA256SUMS, and the CHANGELOG.md section whose ## heading names the version as its notes. A SHA256SUMS among the files is checked, then replaced. An existing release for the tag fails.
jobs:
  node:
    uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/node.yaml@v1
    secrets: inherit
    with:
      commands: |
        npm test
        npm run build
      publish: true
      # npm-publish-dirs: |          # workspaces, in dependency order
      #   packages/client
      #   packages/cli
      # version-file: packages/cli/package.json
      # release-files: dist/*        # with npm-publish-dirs: '' for a release only

To consume a package:

# .npmrc
@scope:registry=https://git.kof22.com/api/packages/QRun-IO/npm/
//git.kof22.com/api/packages/QRun-IO/npm/:_authToken=${GITEA_TOKEN}

Binaries for macOS or Windows need native runners; repositories that ship them (for example QRun-IO/qqq-cli) keep their own release workflow.

Android

.gitea/workflows/build.yaml (see QRun-IO/qqq-android):

name: build
on:
  push:
    branches: ['**']
    tags: ['v*']
jobs:
  android:
    uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/android.yaml@v1
    secrets: inherit
    with:
      tasks: |
        testDebugUnitTest lintDebug assembleDebug
      emulator-tasks: connectedDebugAndroidTest
Input Default Meaning
tasks build Gradle invocations, one per line (arguments to ./gradlew)
java-version '21' JDK from the image (8, 17, 21, 25)
commands '' Shell commands run before Gradle
node-version '' Install this Node.js (official tarball, checksum-verified) for commands
emulator-tasks '' Gradle invocations run with a booted emulator (needs /dev/kvm)
emulator-image system-images;android-30;google_apis;x86 AVD system image
publish-tasks '' Run on vX.Y.Z tags with -Pversion=X.Y.Z
runner android-build Runner label (Lore builders)

Runs in the pinned ci-android-runtime image (cimg/android plus the emulator). To publish an AAR, declare a maven { name = "gitea" ... credentials(PasswordCredentials::class) } repository whose URL comes from the giteaMavenUrl Gradle property; the workflow sets it to the owner's Gitea Maven registry with PACKAGES_TOKEN (or the job token).

Windows and macOS

Host jobs (no container) on the native runners. The repository's own commands run with the tools the host provides; nothing is installed.

name: build
on:
  push:
    branches: ['**']
    tags: ['v*']
jobs:
  windows:
    uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/windows.yaml@v1
    secrets: inherit
    with:
      commands: |
        dotnet restore App.slnx --locked-mode
        dotnet build App.slnx -warnaserror --no-restore
        dotnet test tests/App.Tests --no-build
      artifacts: artifacts/release/*.msi
  macos:
    uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/macos.yaml@v1
    secrets: inherit
    with:
      commands: |
        swift build
        swift test
Input Windows macOS Meaning
commands PowerShell 7, one script bash -exo pipefail Required. The first failure fails the build
runner kof-windows-amd64 kof-macos-arm64 QRun-IO repositories use qqq-windows-vm6218 / qqq-macos-arm64
dotnet-version '' Fail unless a host SDK starts with this version
xcode '' Select Xcode by version (/Applications/Xcode_<v>.app) or path via DEVELOPER_DIR
lfs false Fetch Git LFS objects (needs git-lfs on the host)
artifacts '' '' Files or globs, one per line; on v* tags they and a SHA256SUMS are attached to the tag's Gitea release
checksums SHA256SUMS SHA256SUMS Checksum asset name; give each platform its own when several jobs release one tag
  • Trigger on push only. Host runners run repository code on the machine, and push events come only from branches of the repository itself, never forks.
  • Windows: native commands with a non-zero exit fail the build. A .ps1 that reports failure with exit 1 must run as pwsh -NoProfile -File x.ps1; called with & it would not stop the script. Test tools that assert on child exit codes themselves (Pester suites) need $PSNativeCommandUseErrorActionPreference = $false first.
  • Releases use the job token (contents: write); the org PACKAGES_TOKEN got HTTP 403 on the release lookup. Re-running a tag replaces assets of the same name. A Homebrew or Scoop update can read the release's SHA256SUMS.
  • Host limits today: the Windows runner is a batch-logon task with no interactive desktop (no FlaUI UI tests); the macOS runner is a launchd daemon with no GUI session, so xcodebuild test cannot reach testmanagerd (SwiftPM swift test works), and the host has no git-lfs.

Go

.gitea/workflows/build.yaml (see KofTwentyTwo/license-tool):

name: build
on:
  push:
    branches: ['**']
    tags: ['v*']
jobs:
  go:
    uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/go.yaml@v1
    with:
      golangci-lint: true
      race: true
      release-commands: scripts/build-release.sh "${GITHUB_REF_NAME#v}"
      artifacts: dist/*

Every push runs go mod verify, gofmt -l (tracked files), go vet ./..., optionally golangci-lint, and go test ./..., in golang:<go-version>-bookworm with GOTOOLCHAIN=local (a go.mod that needs a newer Go fails, nothing is downloaded).

Input Default Meaning
go-version '1.26' Selects the official golang:<version>-bookworm image
gofmt true Fail when a tracked .go file is not gofmt-clean
golangci-lint false Pinned, sha256-verified golangci-lint v2 with the repository's .golangci.yml
race false go test -race
test-args '' Extra go test flags (for example a coverage profile)
commands '' Extra gates after the tests, one per line (for example a coverage threshold)
release-commands '' Run on v* tags after every gate (cross-compile into dist/)
artifacts '' Files or globs; on v* tags they and a SHA256SUMS go on the tag's Gitea release (job token; re-runs replace same-named assets)
working-directory . Directory with go.mod
runner container-build Runner label

Gradle

jobs:
  gradle:
    uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/gradle.yaml@v1
    with:
      java-version: '17'
      tasks: build
      artifacts: build/distributions/*.zip

Runs ./gradlew in gradle:jdk<java-version> (the image's own Gradle is unused). Before Gradle runs, the wrapper is verified: distributionUrl must be a services.gradle.org distribution, gradle-wrapper.jar must be that version's official jar, and distributionSha256Sum must be set to the official checksum so Gradle verifies the download. Fix a failure with ./gradlew wrapper --gradle-version <v> --gradle-distribution-sha256-sum <sha256>.

Input Default Meaning
tasks build Gradle invocations, one per line
java-version '21' JDK major version (17, 21, 25)
commands '' Shell commands run before Gradle
artifacts '' Files or globs; on v* tags they and a SHA256SUMS go on the tag's Gitea release
runner container-build Runner label

Android projects use android.yaml.

Lint

Repository hygiene for scripts, configuration and infrastructure code. Every check is off until its input is set.

jobs:
  lint:
    uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/lint.yaml@v1
    with:
      shellcheck: 'ops/**/*.sh'
      tofu-fmt: '**/*.tf'
      # vendor/: third-party scripts copied verbatim, not ours to restyle
      exclude: |
        vendor/**
Input Check
shellcheck shellcheck (.shellcheckrc applies)
yamllint yamllint (.yamllint* applies)
json every file parses (python3 -m json.tool)
ruff ruff check (ruff.toml / pyproject.toml applies)
tofu-fmt tofu fmt -check -diff
tofu-validate directories, one per line: tofu init -backend=false and tofu validate (no state, no secrets)
markdownlint markdownlint-cli2 globs (.markdownlint-cli2.* applies)
ansible-lint arguments to ansible-lint (. = the repository; .ansible-lint applies), in its own job
commands extra shell commands in the lint image (Node 22, Python 3, git, curl)
exclude globs removed from every file check

File inputs are git pathspec globs over tracked files (*.sh top level only, **/*.sh any depth); a glob that matches nothing fails. git-crypt files are ciphertext in CI and always skipped. Tools are pinned: shellcheck, OpenTofu, ruff and uv by sha256; yamllint hash-locked; ansible-lint by image digest; markdownlint-cli2 by exact version with its dependency tree frozen to the release day (npm --before, integrity-checked). Exclude only vendored or generated files, with a comment saying why; fix findings in your own files.

Releasing

To publish Do this
A snapshot Merge to develop.
A release candidate git push origin develop:release/X.Y (first RC), then push or merge fixes to release/X.Y. Every push publishes the next X.Y.0-RC.N.
A release Merge release/X.Y to main and develop, then git tag vX.Y.0 && git push origin vX.Y.0.
A hotfix Push to hotfix/<name>; each push publishes patch + 1. Merge back to main and develop.

A push to main never publishes on its own. The tag is the release.

Changing this workflow

Repositories pin @v1, a tag that only moves after a change is proven. QRun-IO/qbit-sftp-data-integration is the canary and calls @main.

  1. Push the change to main.
  2. Prove it on the canary: push a branch there (verify), then re-run its latest develop run (publish), and any other path the change touches.
  3. Move the tag: git tag -fa v1 -m "<why>" <commit> && git push -f origin v1.

A breaking change (new required input, changed behavior for existing repos) gets v2 and a rollout, not a moved v1.

Differences from the orb

  • Publishes to the Gitea registry, not Maven Central, and does not GPG-sign.
  • main only verifies; the vX.Y.Z tag publishes the release, so a version is never published twice.
  • The release:rc PR label flow is gone. Cut a release candidate by pushing release/X.Y.
  • RC and hotfix publishes create a v<version> git tag. Maven builds create no GitHub or Gitea release objects; tags appear on the repository's Releases page. Node builds create a Gitea release only when release-files is set.
  • Not ported yet: security scans (gitleaks, Semgrep, OWASP, CycloneDX), SpotBugs/PMD, the QQQ middleware API-version check and frontend packaging.