Merge pull request #8 from Dvorinka/ci/auto-release
CI / Infra (Compose + Scripts) (push) Canceled after 0s
CI / Backend (Go) (push) Canceled after 0s
CI / Frontend (React) (push) Canceled after 0s
CI / Compose boot smoke (push) Canceled after 0s
CI / Build & publish images (push) Canceled after 0s
CI / Release (push) Canceled after 0s

feat(ci): release pipeline on merge to main
This commit is contained in:
Tomáš Dvořák authored and GitHub committed 2026-09-23 12:08:34 +02:00
commit 84b102a2c6
8 files changed
+131 -11

No files matched your search

+93
View File
@@ -222,3 +222,96 @@ jobs:
ignore-unfixed: true
vuln-type: os,library
severity: CRITICAL,HIGH
release:
name: Release
runs-on: ubuntu-latest
needs: [docker]
if: github.event_name != 'pull_request'
permissions:
contents: write
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Conventional-commit bump. Tags pushed with GITHUB_TOKEN do not
# retrigger this workflow, so images are retagged below instead.
- name: Bump version and push tag
if: github.ref == 'refs/heads/main'
id: tag_version
uses: mathieudutour/[email protected]
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
default_bump: false
- name: Resolve release tag
id: release
run: |
if [ "${{ github.ref_type }}" = "tag" ]; then
tag="${GITHUB_REF_NAME}"
else
tag="${{ steps.tag_version.outputs.new_tag }}"
fi
if [ -z "$tag" ]; then
echo "No releasable commits since last tag - skipping release."
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Setup Go
if: steps.release.outputs.tag != ''
uses: actions/setup-go@v5
with:
go-version-file: app/backend/go.mod
cache: true
- name: Build CLI binaries
if: steps.release.outputs.tag != ''
working-directory: app/backend
env:
VERSION: ${{ steps.release.outputs.tag }}
run: bash ../../scripts/build-cli.sh
- name: Checksums
if: steps.release.outputs.tag != ''
working-directory: app/backend/bin
run: sha256sum containr-* > checksums.txt
- name: Login to GHCR
if: steps.release.outputs.tag != '' && github.ref == 'refs/heads/main'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Tag pushes from github-tag-action do not trigger workflows, so the
# release job retags the just-published :main images in-registry.
- name: Tag release images in GHCR
if: steps.release.outputs.tag != '' && github.ref == 'refs/heads/main'
env:
TAG: ${{ steps.release.outputs.tag }}
run: |
repo="${GITHUB_REPOSITORY,,}"
for img in backend frontend; do
docker buildx imagetools create \
--tag "ghcr.io/${repo}-${img}:${TAG}" \
--tag "ghcr.io/${repo}-${img}:latest" \
"ghcr.io/${repo}-${img}:main"
done
- name: Create GitHub Release
if: steps.release.outputs.tag != ''
working-directory: app/backend/bin
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.release.outputs.tag }}
run: |
# Delete-then-create keeps reruns idempotent: --clobber uploads
# race GitHub's asset tombstones and intermittently 422. The git
# tag itself is kept (no --cleanup-tag).
gh release delete "$TAG" --yes 2>/dev/null || true
gh release create "$TAG" --title "$TAG" --generate-notes \
containr-* checksums.txt
+8
View File
@@ -6,7 +6,15 @@ import (
"os"
)
// Injected at build time via -ldflags (see scripts/build-cli.sh).
var (
Version = "dev"
BuildTime = "unknown"
GitCommit = "unknown"
)
func main() {
cli.SetVersion(fmt.Sprintf("%s (%s, %s)", Version, GitCommit, BuildTime))
if err := cli.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
+6
View File
@@ -27,6 +27,12 @@ func Execute() {
cobra.CheckErr(rootCmd.Execute())
}
// SetVersion overrides the version reported by `containr --version`.
// Called by main with the value injected via -ldflags at build time.
func SetVersion(v string) {
rootCmd.Version = v
}
func init() {
cobra.OnInitialize(initConfig)
@@ -1,13 +1,26 @@
// Bundles the repository docs/ tree into a generated TS module so the docs
// browser renders instantly and offline. The GitHub sync layer refreshes it
// at runtime when network is available.
import { readdirSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..', '..');
const frontendRoot = join(fileURLToPath(new URL('.', import.meta.url)), '..');
const root = join(frontendRoot, '..', '..');
const docsDir = join(root, 'docs');
const outFile = join(root, 'app/frontend/src/generated/docs-snapshot.ts');
const outFile = join(frontendRoot, 'src/generated/docs-snapshot.ts');
// The published frontend image builds with only app/frontend as its Docker
// context, so the repo docs tree is absent there — reuse the committed
// snapshot instead of failing on ENOENT.
if (!existsSync(docsDir)) {
if (!existsSync(outFile)) {
console.error('docs snapshot: docs/ missing and no committed snapshot to reuse');
process.exit(1);
}
console.warn('docs snapshot: docs/ not found, reusing committed snapshot');
process.exit(0);
}
// Directories that are internal working notes rather than user documentation.
const SKIP_DIRS = new Set(['archive', 'design', 'superpowers']);
@@ -28,7 +28,7 @@ export function LandingPage() {
const sessionQuery = useAuthSession();
const signedIn = Boolean(sessionQuery.data);
const projectsQuery = useQuery({ queryKey: ['landing-projects'], queryFn: listProjects, staleTime: 60_000 });
const projectsQuery = useQuery({ queryKey: ['landing-projects'], queryFn: () => listProjects(), staleTime: 60_000 });
const templatesQuery = useQuery({ queryKey: ['landing-templates'], queryFn: () => listTemplates(), staleTime: 60_000 });
const projects = projectsQuery.data ?? [];
@@ -160,7 +160,7 @@ export function ProjectsPage() {
const projectsQuery = useQuery({
queryKey: ['projects'],
enabled: !isDemoMode,
queryFn: listProjects,
queryFn: () => listProjects(),
});
const hostQuery = useQuery({
queryKey: ['host-monitoring'],
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import {
getApiBaseUrl,
getAgentPublicBaseUrl,
@@ -698,7 +698,7 @@ export function PeoplePage() {
const projectsQuery = useQuery({
queryKey: ['people-projects'],
queryFn: listProjects,
queryFn: () => listProjects(),
});
const auditLogsQuery = useQuery({
queryKey: ['people-audit-logs'],
@@ -1926,7 +1926,7 @@ function BindDatabasePanel({
const [varKey, setVarKey] = useState('DATABASE_URL');
const [bound, setBound] = useState(false);
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: listProjects });
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects() });
const servicesQuery = useQuery({
queryKey: ['services', projectId],
queryFn: () => listServicesByProject(projectId),
@@ -2097,7 +2097,7 @@ export function HighAvailabilityPage() {
const policiesQuery = useQuery({ queryKey: ['ha-policies'], queryFn: listFailoverPolicies });
const alertsQuery = useQuery({ queryKey: ['ha-alerts'], queryFn: listActiveAlerts, refetchInterval: 15_000 });
const healthQuery = useQuery({ queryKey: ['ha-health'], queryFn: listHealthResults, refetchInterval: 30_000 });
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: listProjects });
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects() });
const servicesQuery = useQuery({
queryKey: ['services', policyProjectId],
queryFn: () => listServicesByProject(policyProjectId),
@@ -2515,7 +2515,7 @@ export function SecurityPage() {
const [scanType, setScanType] = useState<string>('comprehensive');
const [scanServiceId, setScanServiceId] = useState('');
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: listProjects });
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects() });
const servicesQuery = useQuery({
queryKey: ['services', projectId],
queryFn: () => listServicesByProject(projectId),
@@ -470,7 +470,7 @@ export function TemplatesPage() {
const projectsQuery = useQuery({
queryKey: ['template-projects'],
enabled: !isDemoMode,
queryFn: listProjects,
queryFn: () => listProjects(),
});
const templates = useMemo(