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:
5
.github/workflows/ci.yaml
vendored
5
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
5
.github/workflows/slack-notify.yaml
vendored
5
.github/workflows/slack-notify.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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...,
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user