mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-29 08:04:53 +00:00
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
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:
8 files changed
+131
-11
No files matched your search
@@ -222,3 +222,96 @@ jobs:
|
|||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
vuln-type: os,library
|
vuln-type: os,library
|
||||||
severity: CRITICAL,HIGH
|
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
|
||||||
@@ -6,7 +6,15 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Injected at build time via -ldflags (see scripts/build-cli.sh).
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
BuildTime = "unknown"
|
||||||
|
GitCommit = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
cli.SetVersion(fmt.Sprintf("%s (%s, %s)", Version, GitCommit, BuildTime))
|
||||||
if err := cli.Run(); err != nil {
|
if err := cli.Run(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ func Execute() {
|
|||||||
cobra.CheckErr(rootCmd.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() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
// Bundles the repository docs/ tree into a generated TS module so the docs
|
// Bundles the repository docs/ tree into a generated TS module so the docs
|
||||||
// browser renders instantly and offline. The GitHub sync layer refreshes it
|
// browser renders instantly and offline. The GitHub sync layer refreshes it
|
||||||
// at runtime when network is available.
|
// 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 { join, relative } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
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 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.
|
// Directories that are internal working notes rather than user documentation.
|
||||||
const SKIP_DIRS = new Set(['archive', 'design', 'superpowers']);
|
const SKIP_DIRS = new Set(['archive', 'design', 'superpowers']);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function LandingPage() {
|
|||||||
const sessionQuery = useAuthSession();
|
const sessionQuery = useAuthSession();
|
||||||
const signedIn = Boolean(sessionQuery.data);
|
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 templatesQuery = useQuery({ queryKey: ['landing-templates'], queryFn: () => listTemplates(), staleTime: 60_000 });
|
||||||
|
|
||||||
const projects = projectsQuery.data ?? [];
|
const projects = projectsQuery.data ?? [];
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export function ProjectsPage() {
|
|||||||
const projectsQuery = useQuery({
|
const projectsQuery = useQuery({
|
||||||
queryKey: ['projects'],
|
queryKey: ['projects'],
|
||||||
enabled: !isDemoMode,
|
enabled: !isDemoMode,
|
||||||
queryFn: listProjects,
|
queryFn: () => listProjects(),
|
||||||
});
|
});
|
||||||
const hostQuery = useQuery({
|
const hostQuery = useQuery({
|
||||||
queryKey: ['host-monitoring'],
|
queryKey: ['host-monitoring'],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
getApiBaseUrl,
|
getApiBaseUrl,
|
||||||
getAgentPublicBaseUrl,
|
getAgentPublicBaseUrl,
|
||||||
@@ -698,7 +698,7 @@ export function PeoplePage() {
|
|||||||
|
|
||||||
const projectsQuery = useQuery({
|
const projectsQuery = useQuery({
|
||||||
queryKey: ['people-projects'],
|
queryKey: ['people-projects'],
|
||||||
queryFn: listProjects,
|
queryFn: () => listProjects(),
|
||||||
});
|
});
|
||||||
const auditLogsQuery = useQuery({
|
const auditLogsQuery = useQuery({
|
||||||
queryKey: ['people-audit-logs'],
|
queryKey: ['people-audit-logs'],
|
||||||
@@ -1926,7 +1926,7 @@ function BindDatabasePanel({
|
|||||||
const [varKey, setVarKey] = useState('DATABASE_URL');
|
const [varKey, setVarKey] = useState('DATABASE_URL');
|
||||||
const [bound, setBound] = useState(false);
|
const [bound, setBound] = useState(false);
|
||||||
|
|
||||||
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: listProjects });
|
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects() });
|
||||||
const servicesQuery = useQuery({
|
const servicesQuery = useQuery({
|
||||||
queryKey: ['services', projectId],
|
queryKey: ['services', projectId],
|
||||||
queryFn: () => listServicesByProject(projectId),
|
queryFn: () => listServicesByProject(projectId),
|
||||||
@@ -2097,7 +2097,7 @@ export function HighAvailabilityPage() {
|
|||||||
const policiesQuery = useQuery({ queryKey: ['ha-policies'], queryFn: listFailoverPolicies });
|
const policiesQuery = useQuery({ queryKey: ['ha-policies'], queryFn: listFailoverPolicies });
|
||||||
const alertsQuery = useQuery({ queryKey: ['ha-alerts'], queryFn: listActiveAlerts, refetchInterval: 15_000 });
|
const alertsQuery = useQuery({ queryKey: ['ha-alerts'], queryFn: listActiveAlerts, refetchInterval: 15_000 });
|
||||||
const healthQuery = useQuery({ queryKey: ['ha-health'], queryFn: listHealthResults, refetchInterval: 30_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({
|
const servicesQuery = useQuery({
|
||||||
queryKey: ['services', policyProjectId],
|
queryKey: ['services', policyProjectId],
|
||||||
queryFn: () => listServicesByProject(policyProjectId),
|
queryFn: () => listServicesByProject(policyProjectId),
|
||||||
@@ -2515,7 +2515,7 @@ export function SecurityPage() {
|
|||||||
const [scanType, setScanType] = useState<string>('comprehensive');
|
const [scanType, setScanType] = useState<string>('comprehensive');
|
||||||
const [scanServiceId, setScanServiceId] = useState('');
|
const [scanServiceId, setScanServiceId] = useState('');
|
||||||
|
|
||||||
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: listProjects });
|
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects() });
|
||||||
const servicesQuery = useQuery({
|
const servicesQuery = useQuery({
|
||||||
queryKey: ['services', projectId],
|
queryKey: ['services', projectId],
|
||||||
queryFn: () => listServicesByProject(projectId),
|
queryFn: () => listServicesByProject(projectId),
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ export function TemplatesPage() {
|
|||||||
const projectsQuery = useQuery({
|
const projectsQuery = useQuery({
|
||||||
queryKey: ['template-projects'],
|
queryKey: ['template-projects'],
|
||||||
enabled: !isDemoMode,
|
enabled: !isDemoMode,
|
||||||
queryFn: listProjects,
|
queryFn: () => listProjects(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const templates = useMemo(
|
const templates = useMemo(
|
||||||
|
|||||||
Reference in new issue
Block a user