1
0
mirror of https://github.com/siderolabs/kres.git synced 2026-02-05 09:45:35 +01:00

feat: use runner groups

Use runner groups for GitHub action to work with GHA runner scale sets.

Support `string`, `array` and `object` types for `runs-on` github action workflow syntax.

Signed-off-by: Noel Georgi <git@frezbo.dev>
This commit is contained in:
Noel Georgi
2025-09-11 10:57:54 +05:30
parent ba566731c8
commit 953994cf35
10 changed files with 126 additions and 83 deletions

View File

@@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2025-08-21T07:54:01Z by kres 18c31cf-dirty.
# Generated on 2025-09-11T00:53:48Z by kres ba56673-dirty.
concurrency:
group: ${{ github.head_ref || github.run_id }}
@@ -27,8 +27,7 @@ jobs:
packages: write
pull-requests: read
runs-on:
- self-hosted
- generic
group: generic
if: (!startsWith(github.head_ref, 'renovate/') && !startsWith(github.head_ref, 'dependabot/'))
steps:
- name: gather-system-info

View File

@@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2025-08-05T08:47:34Z by kres 95bf7d7-dirty.
# Generated on 2025-09-11T08:31:52Z by kres cb448bc-dirty.
"on":
workflow_run:
@@ -14,8 +14,7 @@ name: slack-notify-failure
jobs:
slack-notify:
runs-on:
- self-hosted
- generic
group: generic
if: github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event != 'pull_request'
steps:
- name: Slack Notify

View File

@@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2025-08-04T12:39:36Z by kres 5fb5b90-dirty.
# Generated on 2025-09-11T08:31:52Z by kres cb448bc-dirty.
"on":
workflow_run:
@@ -12,8 +12,7 @@ name: slack-notify
jobs:
slack-notify:
runs-on:
- self-hosted
- generic
group: generic
if: github.event.workflow_run.conclusion != 'skipped'
steps:
- name: Get PR number

View File

@@ -22,8 +22,6 @@ import (
)
const (
// HostedRunner is the name of the hosted runner.
HostedRunner = "self-hosted"
// GenericRunner is the name of the generic runner.
GenericRunner = "generic"
// PkgsRunner is the name of the default runner for packages.
@@ -128,10 +126,9 @@ func NewOutput(mainBranch string, withDefaultJob bool, withStaleJob bool, slackC
},
Jobs: map[string]*Job{
"slack-notify": {
RunsOn: []string{
HostedRunner,
GenericRunner,
},
RunsOn: RunsOn{value: RunsOnGroupLabel{
Group: GenericRunner,
}},
If: "github.event.workflow_run.conclusion != 'skipped'",
Steps: []*JobStep{
Step("Get PR number").
@@ -159,10 +156,9 @@ func NewOutput(mainBranch string, withDefaultJob bool, withStaleJob bool, slackC
},
Jobs: map[string]*Job{
"slack-notify": {
RunsOn: []string{
HostedRunner,
GenericRunner,
},
RunsOn: RunsOn{value: RunsOnGroupLabel{
Group: GenericRunner,
}},
If: "github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event != 'pull_request'",
Steps: []*JobStep{
Step("Slack Notify").
@@ -191,7 +187,7 @@ func NewOutput(mainBranch string, withDefaultJob bool, withStaleJob bool, slackC
},
Jobs: map[string]*Job{
"action": {
RunsOn: []string{"ubuntu-latest"},
RunsOn: RunsOn{[]string{"ubuntu-latest"}},
Steps: []*JobStep{
{
Name: "Lock old issues",
@@ -222,7 +218,7 @@ func NewOutput(mainBranch string, withDefaultJob bool, withStaleJob bool, slackC
},
Jobs: map[string]*Job{
"stale": {
RunsOn: []string{"ubuntu-latest"},
RunsOn: RunsOn{[]string{"ubuntu-latest"}},
Steps: []*JobStep{
{
Name: "Close stale issues and PRs",
@@ -345,25 +341,26 @@ func (o *Output) AddSlackNotifyForFailure(workflow string) {
o.workflows[SlackCIFailureWorkflow].Workflows = append(o.workflows[SlackCIFailureWorkflow].Workflows, workflow)
}
// SetRunners allows to set custom runners for the default job.
// If runners are not provided, the default runners will be used.
func (o *Output) SetRunners(runners ...string) {
if len(runners) == 0 {
o.workflows[CiWorkflow].Jobs["default"].RunsOn = []string{
HostedRunner,
GenericRunner,
}
// SetRunnerGroup allows to set custom runners for the default job.
// If runner is empty, it will be set to "generic".
func (o *Output) SetRunnerGroup(runner string) {
if runner == "" {
o.workflows[CiWorkflow].Jobs["default"].RunsOn = RunsOn{value: RunsOnGroupLabel{
Group: GenericRunner,
}}
return
}
o.workflows[CiWorkflow].Jobs["default"].RunsOn = runners
o.workflows[CiWorkflow].Jobs["default"].RunsOn = RunsOn{value: RunsOnGroupLabel{
Group: runner,
}}
}
// SetOptionsForPkgs overwrites default job steps and services for pkgs.
// Note that calling this method will overwrite any existing steps.
func (o *Output) SetOptionsForPkgs() {
o.SetRunners(HostedRunner, PkgsRunner)
o.SetRunnerGroup(PkgsRunner)
o.workflows[CiWorkflow].Jobs["default"].Steps = DefaultPkgsSteps()
}

View File

@@ -39,6 +39,7 @@ func (suite *GHWorkflowSuite) TestDefaultWorkflows() {
)
output := ghworkflow.NewOutput(defaultBranch, withDefaultJob, withStaleJob, customSlackChannel) //nolint:typecheck
output.SetRunnerGroup("generic")
var buf bytes.Buffer
@@ -72,7 +73,8 @@ jobs:
issues: read
packages: write
pull-requests: read
runs-on: []
runs-on:
group: generic
if: (!startsWith(github.head_ref, 'renovate/') && !startsWith(github.head_ref, 'dependabot/'))
steps:
- name: gather-system-info

View File

@@ -4,6 +4,8 @@
package ghworkflow
import "fmt"
// Workflow represents Github Actions workflow.
//
//nolint:govet
@@ -68,7 +70,7 @@ type Push struct {
// Job represents GitHub Actions job.
type Job struct {
Permissions map[string]string `yaml:"permissions,omitempty"`
RunsOn []string `yaml:"runs-on"`
RunsOn RunsOn `yaml:"runs-on"`
If string `yaml:"if,omitempty"`
Needs []string `yaml:"needs,omitempty"`
Outputs map[string]string `yaml:"outputs,omitempty"`
@@ -76,6 +78,88 @@ type Job struct {
Steps []*JobStep `yaml:"steps"`
}
// RunsOn represents GitHub Actions runs-on field which can be a string, slice, or type with Group/Label structure.
type RunsOn struct {
value any
}
type RunsOnGroupLabel struct {
Group string `yaml:"group,omitempty"`
Label string `yaml:"label,omitempty"`
}
// MarshalYAML implements yaml.Marshaler.
func (r RunsOn) MarshalYAML() (any, error) {
// check if r.value is nil or empty
if r.value == nil {
return nil, fmt.Errorf("runs-on needs to be set")
}
// check for empty values based on type
switch v := r.value.(type) {
case string:
if v == "" {
return nil, fmt.Errorf("runs-on cannot be empty string")
}
case []string:
if len(v) == 0 {
return nil, fmt.Errorf("runs-on cannot be empty slice")
}
case RunsOnGroupLabel:
if v.Group == "" && v.Label == "" {
return nil, fmt.Errorf("runs-on needs to be set with at least group or label")
}
default:
return nil, fmt.Errorf("runs-on must be a string, slice of strings, or type with group/label, got: %T", r.value)
}
return r.value, nil
}
// UnmarshalYAML implements yaml.Unmarshaler.
func (r *RunsOn) UnmarshalYAML(unmarshal func(any) error) error {
// Try string first
var str string
if err := unmarshal(&str); err == nil {
r.value = str
return nil
}
// Try slice of strings
var slice []string
if err := unmarshal(&slice); err == nil {
r.value = slice
return nil
}
// Try group/label map
var groupLabel RunsOnGroupLabel
if err := unmarshal(&groupLabel); err == nil {
r.value = groupLabel
return nil
}
return fmt.Errorf("runs-on must be a string, slice of strings, or map with group/label, got: %T", r.value)
}
// NewRunsOnString creates a RunsOn from a string.
func NewRunsOnString(runner string) RunsOn {
return RunsOn{value: runner}
}
// NewRunsOnSlice creates a RunsOn from a slice of strings.
func NewRunsOnSlice(runners []string) RunsOn {
return RunsOn{value: runners}
}
// NewRunsOnGroupLabel creates a RunsOn from a group and label.
func NewRunsOnGroupLabel(group, label string) RunsOn {
return RunsOn{value: RunsOnGroupLabel{Group: group, Label: label}}
}
// Service represents GitHub Actions service.
type Service struct {
Image string `yaml:"image"`

View File

@@ -24,7 +24,7 @@ type Job struct {
Conditions []string `yaml:"conditions,omitempty"`
Crons []string `yaml:"crons,omitempty"`
Depends []string `yaml:"depends,omitempty"`
Runners []string `yaml:"runners,omitempty"`
RunnerGroup string `yaml:"runnerGroup,omitempty"`
TriggerLabels []string `yaml:"triggerLabels,omitempty"`
Steps []Step `yaml:"steps,omitempty"`
SOPS bool `yaml:"sops"`
@@ -95,9 +95,9 @@ type RegistryLoginStep struct {
type GHWorkflow struct {
meta *meta.Options
dag.BaseNode
CIFailuresSlackNotifyChannel string `yaml:"ciFailuresSlackNotifyChannel,omitempty"`
CustomRunners []string `yaml:"customRunners,omitempty"`
Jobs []Job `yaml:"jobs"`
CIFailuresSlackNotifyChannel string `yaml:"ciFailuresSlackNotifyChannel,omitempty"`
CustomRunnerGroup string `yaml:"customRunnerGroup,omitempty"`
Jobs []Job `yaml:"jobs"`
}
// NewGHWorkflow creates a new GHWorkflow node.
@@ -114,7 +114,7 @@ func NewGHWorkflow(meta *meta.Options) *GHWorkflow {
//nolint:gocognit,gocyclo,cyclop,maintidx
func (gh *GHWorkflow) CompileGitHubWorkflow(o *ghworkflow.Output) error {
if !gh.meta.CompileGithubWorkflowsOnly {
o.SetRunners(gh.CustomRunners...)
o.SetRunnerGroup(gh.CustomRunnerGroup)
return nil
}
@@ -124,7 +124,7 @@ func (gh *GHWorkflow) CompileGitHubWorkflow(o *ghworkflow.Output) error {
for _, job := range gh.Jobs {
jobDef := &ghworkflow.Job{
If: ghworkflow.DefaultSkipCondition,
RunsOn: job.Runners,
RunsOn: ghworkflow.NewRunsOnGroupLabel(job.RunnerGroup, ""),
Permissions: ghworkflow.DefaultJobPermissions(),
Needs: job.Depends,
Steps: ghworkflow.CommonSteps(),
@@ -443,32 +443,7 @@ func (gh *GHWorkflow) CompileGitHubWorkflow(o *ghworkflow.Output) error {
},
Jobs: map[string]*ghworkflow.Job{
"default": {
RunsOn: job.Runners,
Steps: jobDef.Steps,
},
},
},
)
o.AddWorkflow(
workflowName,
&ghworkflow.Workflow{
Name: workflowName,
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-a-fallback-value
Concurrency: ghworkflow.Concurrency{
Group: "${{ github.head_ref || github.run_id }}",
CancelInProgress: true,
},
On: ghworkflow.On{
Schedule: xslices.Map(job.Crons, func(cron string) ghworkflow.Schedule {
return ghworkflow.Schedule{
Cron: cron,
}
}),
},
Jobs: map[string]*ghworkflow.Job{
"default": {
RunsOn: job.Runners,
RunsOn: ghworkflow.NewRunsOnGroupLabel(job.RunnerGroup, ""),
Services: jobDef.Services,
Steps: jobDef.Steps,
},

View File

@@ -94,7 +94,7 @@ type Step struct {
Name string `yaml:"name"`
EnvironmentOverride map[string]string `yaml:"environmentOverride"`
Crons []string `yaml:"crons"`
RunnerLabels []string `yaml:"runnerLabels"`
RunnerGroup string `yaml:"runnerGroup"`
TriggerLabels []string `yaml:"triggerLabels"`
Artifacts Artifacts `yaml:"artifacts"`
} `yaml:"jobs"`
@@ -401,10 +401,6 @@ func (step *Step) CompileGitHubWorkflow(output *ghworkflow.Output) error {
steps...,
)
runnerLabels := []string{
ghworkflow.HostedRunner,
}
for _, job := range step.GHAction.Jobs {
conditions := xslices.Map(job.TriggerLabels, func(label string) string {
return fmt.Sprintf("contains(fromJSON(needs.default.outputs.labels), '%s')", label)
@@ -475,7 +471,7 @@ func (step *Step) CompileGitHubWorkflow(output *ghworkflow.Output) error {
}
output.AddJob(job.Name, &ghworkflow.Job{
RunsOn: append(runnerLabels, job.RunnerLabels...),
RunsOn: ghworkflow.NewRunsOnGroupLabel(job.RunnerGroup, ""),
If: strings.Join(conditions, " || "),
Needs: []string{"default"},
Steps: defaultSteps,
@@ -553,7 +549,7 @@ func (step *Step) CompileGitHubWorkflow(output *ghworkflow.Output) error {
},
Jobs: map[string]*ghworkflow.Job{
"default": {
RunsOn: append(runnerLabels, job.RunnerLabels...),
RunsOn: ghworkflow.NewRunsOnGroupLabel(job.RunnerGroup, ""),
Steps: append(
defaultSteps,
steps...,

View File

@@ -134,10 +134,7 @@ func (helm *Build) CompileGitHubWorkflow(output *ghworkflow.Output) error {
Jobs: map[string]*ghworkflow.Job{
"default": {
Permissions: jobPermissions,
RunsOn: []string{
ghworkflow.HostedRunner,
ghworkflow.GenericRunner,
},
RunsOn: ghworkflow.NewRunsOnGroupLabel(ghworkflow.GenericRunner, ""),
Steps: slices.Concat(
ghworkflow.CommonSteps(),
[]*ghworkflow.JobStep{

View File

@@ -261,13 +261,8 @@ func (pkgfile *Build) CompileGitHubWorkflow(output *ghworkflow.Output) error {
"labels": "${{ steps.retrieve-pr-labels.outputs.result }}",
})
runnerLabels := []string{
ghworkflow.HostedRunner,
ghworkflow.PkgsRunner,
}
output.AddJob("reproducibility", &ghworkflow.Job{
RunsOn: runnerLabels,
RunsOn: ghworkflow.NewRunsOnGroupLabel(ghworkflow.PkgsRunner, ""),
If: "contains(fromJSON(needs.default.outputs.labels), 'integration/reproducibility')",
Needs: []string{"default"},
Steps: ghworkflow.DefaultPkgsSteps(),
@@ -293,7 +288,7 @@ func (pkgfile *Build) CompileGitHubWorkflow(output *ghworkflow.Output) error {
},
Jobs: map[string]*ghworkflow.Job{
"reproducibility": {
RunsOn: runnerLabels,
RunsOn: ghworkflow.NewRunsOnGroupLabel(ghworkflow.PkgsRunner, ""),
Steps: append(
ghworkflow.DefaultPkgsSteps(),
ghworkflow.Step("reproducibility-test").SetMakeStep("reproducibility-test"),