mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-27 05:55:21 +08:00
Merge branch 'main' into support-theme-select
This commit is contained in:
commit
cb69979425
@ -26,20 +26,16 @@ WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
||||
RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
|
||||
&& make clean-all build
|
||||
|
||||
# Begin env-to-ini build
|
||||
RUN go build contrib/environment-to-ini/environment-to-ini.go
|
||||
|
||||
# Copy local files
|
||||
COPY docker/root /tmp/local
|
||||
|
||||
# Set permissions
|
||||
RUN chmod 755 /tmp/local/usr/bin/entrypoint \
|
||||
/tmp/local/usr/local/bin/gitea \
|
||||
/tmp/local/usr/local/bin/* \
|
||||
/tmp/local/etc/s6/gitea/* \
|
||||
/tmp/local/etc/s6/openssh/* \
|
||||
/tmp/local/etc/s6/.s6-svscan/* \
|
||||
/go/src/code.gitea.io/gitea/gitea \
|
||||
/go/src/code.gitea.io/gitea/environment-to-ini
|
||||
/go/src/code.gitea.io/gitea/gitea
|
||||
|
||||
FROM docker.io/library/alpine:3.22
|
||||
LABEL maintainer="maintainers@gitea.io"
|
||||
@ -82,4 +78,3 @@ CMD ["/usr/bin/s6-svscan", "/etc/s6"]
|
||||
|
||||
COPY --from=build-env /tmp/local /
|
||||
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
||||
COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
|
||||
|
||||
@ -26,18 +26,12 @@ WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
||||
RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
|
||||
&& make clean-all build
|
||||
|
||||
# Begin env-to-ini build
|
||||
RUN go build contrib/environment-to-ini/environment-to-ini.go
|
||||
|
||||
# Copy local files
|
||||
COPY docker/rootless /tmp/local
|
||||
|
||||
# Set permissions
|
||||
RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
|
||||
/tmp/local/usr/local/bin/docker-setup.sh \
|
||||
/tmp/local/usr/local/bin/gitea \
|
||||
/go/src/code.gitea.io/gitea/gitea \
|
||||
/go/src/code.gitea.io/gitea/environment-to-ini
|
||||
RUN chmod 755 /tmp/local/usr/local/bin/* \
|
||||
/go/src/code.gitea.io/gitea/gitea
|
||||
|
||||
FROM docker.io/library/alpine:3.22
|
||||
LABEL maintainer="maintainers@gitea.io"
|
||||
@ -71,7 +65,6 @@ RUN chown git:git /var/lib/gitea /etc/gitea
|
||||
|
||||
COPY --from=build-env /tmp/local /
|
||||
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
||||
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
|
||||
|
||||
# git:git
|
||||
USER 1000:1000
|
||||
|
||||
156
cmd/config.go
Normal file
156
cmd/config.go
Normal file
@ -0,0 +1,156 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func cmdConfig() *cli.Command {
|
||||
subcmdConfigEditIni := &cli.Command{
|
||||
Name: "edit-ini",
|
||||
Usage: "Load an existing INI file, apply environment variables, keep specified keys, and output to a new INI file.",
|
||||
Description: `
|
||||
Help users to edit the Gitea configuration INI file.
|
||||
|
||||
# Keep Specified Keys
|
||||
|
||||
If you need to re-create the configuration file with only a subset of keys,
|
||||
you can provide an INI template file for the kept keys and use the "--config-keep-keys" flag.
|
||||
For example, if a helm chart needs to reset the settings and only keep SECRET_KEY,
|
||||
it can use a template file (only keys take effect, values are ignored):
|
||||
|
||||
[security]
|
||||
SECRET_KEY=
|
||||
|
||||
$ ./gitea config edit-ini --config app-old.ini --config-keep-keys app-keys.ini --out app-new.ini
|
||||
|
||||
# Map Environment Variables to INI Configuration
|
||||
|
||||
Environment variables of the form "GITEA__section_name__KEY_NAME"
|
||||
will be mapped to the ini section "[section_name]" and the key
|
||||
"KEY_NAME" with the value as provided.
|
||||
|
||||
Environment variables of the form "GITEA__section_name__KEY_NAME__FILE"
|
||||
will be mapped to the ini section "[section_name]" and the key
|
||||
"KEY_NAME" with the value loaded from the specified file.
|
||||
|
||||
Environment variable keys can only contain characters "0-9A-Z_",
|
||||
if a section or key name contains dot ".", it needs to be escaped as _0x2E_.
|
||||
For example, to apply this config:
|
||||
|
||||
[git.config]
|
||||
foo.bar=val
|
||||
|
||||
$ export GITEA__git_0x2E_config__foo_0x2E_bar=val
|
||||
|
||||
# Put All Together
|
||||
|
||||
$ ./gitea config edit-ini --config app.ini --config-keep-keys app-keys.ini --apply-env {--in-place|--out app-new.ini}
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
// "--config" flag is provided by global flags, and this flag is also used by "environment-to-ini" script wrapper
|
||||
// "--in-place" is also used by "environment-to-ini" script wrapper for its old behavior: always overwrite the existing config file
|
||||
&cli.BoolFlag{
|
||||
Name: "in-place",
|
||||
Usage: "Output to the same config file as input. This flag will be ignored if --out is set.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config-keep-keys",
|
||||
Usage: "An INI template file containing keys for keeping. Only the keys defined in the INI template will be kept from old config. If not set, all keys will be kept.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "apply-env",
|
||||
Usage: "Apply all GITEA__* variables from the environment to the config.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "out",
|
||||
Usage: "Destination config file to write to.",
|
||||
},
|
||||
},
|
||||
Action: runConfigEditIni,
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
Name: "config",
|
||||
Usage: "Manage Gitea configuration",
|
||||
Commands: []*cli.Command{
|
||||
subcmdConfigEditIni,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runConfigEditIni(_ context.Context, c *cli.Command) error {
|
||||
// the config system may change the environment variables, so get a copy first, to be used later
|
||||
env := append([]string{}, os.Environ()...)
|
||||
|
||||
// don't use the guessed setting.CustomConf, instead, require the user to provide --config explicitly
|
||||
if !c.IsSet("config") {
|
||||
return errors.New("flag is required but not set: --config")
|
||||
}
|
||||
configFileIn := c.String("config")
|
||||
|
||||
cfgIn, err := setting.NewConfigProviderFromFile(configFileIn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config file %q: %v", configFileIn, err)
|
||||
}
|
||||
|
||||
// determine output config file: use "--out" flag or use "--in-place" flag to overwrite input file
|
||||
inPlace := c.Bool("in-place")
|
||||
configFileOut := c.String("out")
|
||||
if configFileOut == "" {
|
||||
if !inPlace {
|
||||
return errors.New("either --in-place or --out must be specified")
|
||||
}
|
||||
configFileOut = configFileIn // in-place edit
|
||||
}
|
||||
|
||||
needWriteOut := configFileOut != configFileIn
|
||||
|
||||
cfgOut := cfgIn
|
||||
configKeepKeys := c.String("config-keep-keys")
|
||||
if configKeepKeys != "" {
|
||||
needWriteOut = true
|
||||
cfgOut, err = setting.NewConfigProviderFromFile(configKeepKeys)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config-keep-keys template file %q: %v", configKeepKeys, err)
|
||||
}
|
||||
|
||||
for _, secOut := range cfgOut.Sections() {
|
||||
for _, keyOut := range secOut.Keys() {
|
||||
secIn := cfgIn.Section(secOut.Name())
|
||||
keyIn := setting.ConfigSectionKey(secIn, keyOut.Name())
|
||||
if keyIn != nil {
|
||||
keyOut.SetValue(keyIn.String())
|
||||
} else {
|
||||
secOut.DeleteKey(keyOut.Name())
|
||||
}
|
||||
}
|
||||
if len(secOut.Keys()) == 0 {
|
||||
cfgOut.DeleteSection(secOut.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("apply-env") {
|
||||
if setting.EnvironmentToConfig(cfgOut, env) {
|
||||
needWriteOut = true
|
||||
}
|
||||
}
|
||||
|
||||
if needWriteOut {
|
||||
err = cfgOut.SaveTo(configFileOut)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
85
cmd/config_test.go
Normal file
85
cmd/config_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigEdit(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configOld := tmpDir + "/app-old.ini"
|
||||
configTemplate := tmpDir + "/app-template.ini"
|
||||
_ = os.WriteFile(configOld, []byte(`
|
||||
[sec]
|
||||
k1=v1
|
||||
k2=v2
|
||||
`), os.ModePerm)
|
||||
|
||||
_ = os.WriteFile(configTemplate, []byte(`
|
||||
[sec]
|
||||
k1=in-template
|
||||
|
||||
[sec2]
|
||||
k3=v3
|
||||
`), os.ModePerm)
|
||||
|
||||
t.Setenv("GITEA__EnV__KeY", "val")
|
||||
|
||||
t.Run("OutputToNewWithEnv", func(t *testing.T) {
|
||||
configNew := tmpDir + "/app-new.ini"
|
||||
err := NewMainApp(AppVersion{}).Run(t.Context(), []string{
|
||||
"./gitea", "--config", configOld,
|
||||
"config", "edit-ini",
|
||||
"--apply-env",
|
||||
"--config-keep-keys", configTemplate,
|
||||
"--out", configNew,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// "k1" old value is kept because its key is in the template
|
||||
// "k2" is removed because it isn't in the template
|
||||
// "k3" isn't in new config because it isn't in the old config
|
||||
// [env] is applied from environment variable
|
||||
data, _ := os.ReadFile(configNew)
|
||||
require.Equal(t, `[sec]
|
||||
k1 = v1
|
||||
|
||||
[env]
|
||||
KeY = val
|
||||
`, string(data))
|
||||
})
|
||||
|
||||
t.Run("OutputToExisting(environment-to-ini)", func(t *testing.T) {
|
||||
// the legacy "environment-to-ini" (now a wrapper script) behavior:
|
||||
// if no "--out", then "--in-place" must be used to overwrite the existing "--config" file
|
||||
err := NewMainApp(AppVersion{}).Run(t.Context(), []string{
|
||||
"./gitea", "config", "edit-ini",
|
||||
"--apply-env",
|
||||
"--config", configOld,
|
||||
})
|
||||
require.ErrorContains(t, err, "either --in-place or --out must be specified")
|
||||
|
||||
// simulate the "environment-to-ini" behavior with "--in-place"
|
||||
err = NewMainApp(AppVersion{}).Run(t.Context(), []string{
|
||||
"./gitea", "config", "edit-ini",
|
||||
"--in-place",
|
||||
"--apply-env",
|
||||
"--config", configOld,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
data, _ := os.ReadFile(configOld)
|
||||
require.Equal(t, `[sec]
|
||||
k1 = v1
|
||||
k2 = v2
|
||||
|
||||
[env]
|
||||
KeY = val
|
||||
`, string(data))
|
||||
})
|
||||
}
|
||||
@ -128,6 +128,7 @@ func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
|
||||
// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
|
||||
subCmdStandalone := []*cli.Command{
|
||||
cmdConfig(),
|
||||
cmdCert(),
|
||||
CmdGenerate,
|
||||
CmdDocs,
|
||||
|
||||
@ -156,7 +156,6 @@ func serveInstall(cmd *cli.Command) error {
|
||||
case <-graceful.GetManager().IsShutdown():
|
||||
<-graceful.GetManager().Done()
|
||||
log.Info("PID: %d Gitea Web Finished", os.Getpid())
|
||||
log.GetManager().Close()
|
||||
return err
|
||||
default:
|
||||
}
|
||||
@ -231,7 +230,6 @@ func serveInstalled(c *cli.Command) error {
|
||||
err := listen(webRoutes, true)
|
||||
<-graceful.GetManager().Done()
|
||||
log.Info("PID: %d Gitea Web Finished", os.Getpid())
|
||||
log.GetManager().Close()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
Environment To Ini
|
||||
==================
|
||||
|
||||
Multiple docker users have requested that the Gitea docker is changed
|
||||
to permit arbitrary configuration via environment variables.
|
||||
|
||||
Gitea needs to use an ini file for configuration because the running
|
||||
environment that starts the docker may not be the same as that used
|
||||
by the hooks. An ini file also gives a good default and means that
|
||||
users do not have to completely provide a full environment.
|
||||
|
||||
With those caveats above, this command provides a generic way of
|
||||
converting suitably structured environment variables into any ini
|
||||
value.
|
||||
|
||||
To use the command is very simple just run it and the default gitea
|
||||
app.ini will be rewritten to take account of the variables provided,
|
||||
however there are various options to give slightly different
|
||||
behavior and these can be interrogated with the `-h` option.
|
||||
|
||||
The environment variables should be of the form:
|
||||
|
||||
GITEA__SECTION_NAME__KEY_NAME
|
||||
|
||||
Note, SECTION_NAME in the notation above is case-insensitive.
|
||||
|
||||
Environment variables are usually restricted to a reduced character
|
||||
set "0-9A-Z_" - in order to allow the setting of sections with
|
||||
characters outside of that set, they should be escaped as following:
|
||||
"_0X2E_" for "." and "_0X2D_" for "-". The entire section and key names
|
||||
can be escaped as a UTF8 byte string if necessary. E.g. to configure:
|
||||
|
||||
"""
|
||||
...
|
||||
[log.console]
|
||||
COLORIZE=false
|
||||
STDERR=true
|
||||
...
|
||||
"""
|
||||
|
||||
You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false"
|
||||
and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found
|
||||
on the configuration cheat sheet.
|
||||
|
||||
To build locally, run:
|
||||
|
||||
go build contrib/environment-to-ini/environment-to-ini.go
|
||||
@ -1,112 +0,0 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.Command{}
|
||||
app.Name = "environment-to-ini"
|
||||
app.Usage = "Use provided environment to update configuration ini"
|
||||
app.Description = `As a helper to allow docker users to update the gitea configuration
|
||||
through the environment, this command allows environment variables to
|
||||
be mapped to values in the ini.
|
||||
|
||||
Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME"
|
||||
will be mapped to the ini section "[section_name]" and the key
|
||||
"KEY_NAME" with the value as provided.
|
||||
|
||||
Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME__FILE"
|
||||
will be mapped to the ini section "[section_name]" and the key
|
||||
"KEY_NAME" with the value loaded from the specified file.
|
||||
|
||||
Environment variables are usually restricted to a reduced character
|
||||
set "0-9A-Z_" - in order to allow the setting of sections with
|
||||
characters outside of that set, they should be escaped as following:
|
||||
"_0X2E_" for ".". The entire section and key names can be escaped as
|
||||
a UTF8 byte string if necessary. E.g. to configure:
|
||||
|
||||
"""
|
||||
...
|
||||
[log.console]
|
||||
COLORIZE=false
|
||||
STDERR=true
|
||||
...
|
||||
"""
|
||||
|
||||
You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false"
|
||||
and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found
|
||||
on the configuration cheat sheet.`
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "custom-path",
|
||||
Aliases: []string{"C"},
|
||||
Value: setting.CustomPath,
|
||||
Usage: "Custom path file path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
Aliases: []string{"c"},
|
||||
Value: setting.CustomConf,
|
||||
Usage: "Custom configuration file path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "work-path",
|
||||
Aliases: []string{"w"},
|
||||
Value: setting.AppWorkPath,
|
||||
Usage: "Set the gitea working path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "out",
|
||||
Aliases: []string{"o"},
|
||||
Value: "",
|
||||
Usage: "Destination file to write to",
|
||||
},
|
||||
}
|
||||
app.Action = runEnvironmentToIni
|
||||
err := app.Run(context.Background(), os.Args)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to run app with %s: %v", os.Args, err)
|
||||
}
|
||||
}
|
||||
|
||||
func runEnvironmentToIni(_ context.Context, c *cli.Command) error {
|
||||
// the config system may change the environment variables, so get a copy first, to be used later
|
||||
env := append([]string{}, os.Environ()...)
|
||||
setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{
|
||||
WorkPath: c.String("work-path"),
|
||||
CustomPath: c.String("custom-path"),
|
||||
CustomConf: c.String("config"),
|
||||
})
|
||||
|
||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err)
|
||||
}
|
||||
|
||||
changed := setting.EnvironmentToConfig(cfg, env)
|
||||
|
||||
// try to save the config file
|
||||
destination := c.String("out")
|
||||
if len(destination) == 0 {
|
||||
destination = setting.CustomConf
|
||||
}
|
||||
if destination != setting.CustomConf || changed {
|
||||
log.Info("Settings saved to: %q", destination)
|
||||
err = cfg.SaveTo(destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
2
docker/root/usr/local/bin/environment-to-ini
Normal file
2
docker/root/usr/local/bin/environment-to-ini
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
exec /app/gitea/gitea config edit-ini --in-place --apply-env "$@"
|
||||
2
docker/rootless/usr/local/bin/environment-to-ini
Normal file
2
docker/rootless/usr/local/bin/environment-to-ini
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
exec /app/gitea/gitea config edit-ini --in-place --apply-env "$@"
|
||||
1
main.go
1
main.go
@ -44,6 +44,7 @@ func main() {
|
||||
}
|
||||
app := cmd.NewMainApp(cmd.AppVersion{Version: Version, Extra: formatBuiltWith()})
|
||||
_ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp
|
||||
// flush the queued logs before exiting, it is a MUST, otherwise there will be log loss
|
||||
log.GetManager().Close()
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/shared/types"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@ -173,6 +174,13 @@ func (r *ActionRunner) GenerateToken() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// CanMatchLabels checks whether the runner's labels can match a job's "runs-on"
|
||||
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on
|
||||
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
|
||||
runnerLabelSet := container.SetOf(r.AgentLabels...)
|
||||
return runnerLabelSet.Contains(jobRunsOn...) // match all labels
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(&ActionRunner{})
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import (
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@ -245,7 +244,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
||||
var job *ActionRunJob
|
||||
log.Trace("runner labels: %v", runner.AgentLabels)
|
||||
for _, v := range jobs {
|
||||
if isSubset(runner.AgentLabels, v.RunsOn) {
|
||||
if runner.CanMatchLabels(v.RunsOn) {
|
||||
job = v
|
||||
break
|
||||
}
|
||||
@ -475,20 +474,6 @@ func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, lim
|
||||
Find(&tasks)
|
||||
}
|
||||
|
||||
func isSubset(set, subset []string) bool {
|
||||
m := make(container.Set[string], len(set))
|
||||
for _, v := range set {
|
||||
m.Add(v)
|
||||
}
|
||||
|
||||
for _, v := range subset {
|
||||
if !m.Contains(v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp {
|
||||
if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 {
|
||||
return timeutil.TimeStamp(0)
|
||||
|
||||
@ -11,7 +11,6 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@ -30,12 +29,15 @@ type ServeFunction = func(net.Listener) error
|
||||
|
||||
// Server represents our graceful server
|
||||
type Server struct {
|
||||
network string
|
||||
address string
|
||||
listener net.Listener
|
||||
wg sync.WaitGroup
|
||||
state state
|
||||
lock *sync.RWMutex
|
||||
network string
|
||||
address string
|
||||
listener net.Listener
|
||||
|
||||
lock sync.RWMutex
|
||||
state state
|
||||
connCounter int64
|
||||
connEmptyCond *sync.Cond
|
||||
|
||||
BeforeBegin func(network, address string)
|
||||
OnShutdown func()
|
||||
PerWriteTimeout time.Duration
|
||||
@ -50,14 +52,13 @@ func NewServer(network, address, name string) *Server {
|
||||
log.Info("Starting new %s server: %s:%s on PID: %d", name, network, address, os.Getpid())
|
||||
}
|
||||
srv := &Server{
|
||||
wg: sync.WaitGroup{},
|
||||
state: stateInit,
|
||||
lock: &sync.RWMutex{},
|
||||
network: network,
|
||||
address: address,
|
||||
PerWriteTimeout: setting.PerWriteTimeout,
|
||||
PerWritePerKbTimeout: setting.PerWritePerKbTimeout,
|
||||
}
|
||||
srv.connEmptyCond = sync.NewCond(&srv.lock)
|
||||
|
||||
srv.BeforeBegin = func(network, addr string) {
|
||||
log.Debug("Starting server on %s:%s (PID: %d)", network, addr, syscall.Getpid())
|
||||
@ -154,7 +155,7 @@ func (srv *Server) Serve(serve ServeFunction) error {
|
||||
GetManager().RegisterServer()
|
||||
err := serve(srv.listener)
|
||||
log.Debug("Waiting for connections to finish... (PID: %d)", syscall.Getpid())
|
||||
srv.wg.Wait()
|
||||
srv.waitForActiveConnections()
|
||||
srv.setState(stateTerminate)
|
||||
GetManager().ServerDone()
|
||||
// use of closed means that the listeners are closed - i.e. we should be shutting down - return nil
|
||||
@ -178,16 +179,62 @@ func (srv *Server) setState(st state) {
|
||||
srv.state = st
|
||||
}
|
||||
|
||||
func (srv *Server) waitForActiveConnections() {
|
||||
srv.lock.Lock()
|
||||
for srv.connCounter > 0 {
|
||||
srv.connEmptyCond.Wait()
|
||||
}
|
||||
srv.lock.Unlock()
|
||||
}
|
||||
|
||||
func (srv *Server) wrapConnection(c net.Conn) (net.Conn, error) {
|
||||
srv.lock.Lock()
|
||||
defer srv.lock.Unlock()
|
||||
|
||||
if srv.state != stateRunning {
|
||||
_ = c.Close()
|
||||
return nil, syscall.EINVAL // same as AcceptTCP
|
||||
}
|
||||
|
||||
srv.connCounter++
|
||||
return &wrappedConn{Conn: c, server: srv}, nil
|
||||
}
|
||||
|
||||
func (srv *Server) removeConnection(_ *wrappedConn) {
|
||||
srv.lock.Lock()
|
||||
defer srv.lock.Unlock()
|
||||
|
||||
srv.connCounter--
|
||||
if srv.connCounter <= 0 {
|
||||
srv.connEmptyCond.Broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
// closeAllConnections forcefully closes all active connections
|
||||
func (srv *Server) closeAllConnections() {
|
||||
srv.lock.Lock()
|
||||
if srv.connCounter > 0 {
|
||||
log.Warn("After graceful shutdown period, %d connections are still active. Forcefully close.", srv.connCounter)
|
||||
srv.connCounter = 0 // OS will close all the connections after the process exits, so we just assume there is no active connection now
|
||||
}
|
||||
srv.lock.Unlock()
|
||||
srv.connEmptyCond.Broadcast()
|
||||
}
|
||||
|
||||
type filer interface {
|
||||
File() (*os.File, error)
|
||||
}
|
||||
|
||||
type wrappedListener struct {
|
||||
net.Listener
|
||||
stopped bool
|
||||
server *Server
|
||||
server *Server
|
||||
}
|
||||
|
||||
var (
|
||||
_ net.Listener = (*wrappedListener)(nil)
|
||||
_ filer = (*wrappedListener)(nil)
|
||||
)
|
||||
|
||||
func newWrappedListener(l net.Listener, srv *Server) *wrappedListener {
|
||||
return &wrappedListener{
|
||||
Listener: l,
|
||||
@ -195,46 +242,24 @@ func newWrappedListener(l net.Listener, srv *Server) *wrappedListener {
|
||||
}
|
||||
}
|
||||
|
||||
func (wl *wrappedListener) Accept() (net.Conn, error) {
|
||||
var c net.Conn
|
||||
// Set keepalive on TCPListeners connections.
|
||||
func (wl *wrappedListener) Accept() (c net.Conn, err error) {
|
||||
if tcl, ok := wl.Listener.(*net.TCPListener); ok {
|
||||
// Set keepalive on TCPListeners connections if possible, see http.tcpKeepAliveListener
|
||||
tc, err := tcl.AcceptTCP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = tc.SetKeepAlive(true) // see http.tcpKeepAliveListener
|
||||
_ = tc.SetKeepAlivePeriod(3 * time.Minute) // see http.tcpKeepAliveListener
|
||||
_ = tc.SetKeepAlive(true)
|
||||
_ = tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
c = tc
|
||||
} else {
|
||||
var err error
|
||||
c, err = wl.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
closed := int32(0)
|
||||
|
||||
c = &wrappedConn{
|
||||
Conn: c,
|
||||
server: wl.server,
|
||||
closed: &closed,
|
||||
perWriteTimeout: wl.server.PerWriteTimeout,
|
||||
perWritePerKbTimeout: wl.server.PerWritePerKbTimeout,
|
||||
}
|
||||
|
||||
wl.server.wg.Add(1)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (wl *wrappedListener) Close() error {
|
||||
if wl.stopped {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
|
||||
wl.stopped = true
|
||||
return wl.Listener.Close()
|
||||
return wl.server.wrapConnection(c)
|
||||
}
|
||||
|
||||
func (wl *wrappedListener) File() (*os.File, error) {
|
||||
@ -244,17 +269,14 @@ func (wl *wrappedListener) File() (*os.File, error) {
|
||||
|
||||
type wrappedConn struct {
|
||||
net.Conn
|
||||
server *Server
|
||||
closed *int32
|
||||
deadline time.Time
|
||||
perWriteTimeout time.Duration
|
||||
perWritePerKbTimeout time.Duration
|
||||
server *Server
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Write(p []byte) (n int, err error) {
|
||||
if w.perWriteTimeout > 0 {
|
||||
minTimeout := time.Duration(len(p)/1024) * w.perWritePerKbTimeout
|
||||
minDeadline := time.Now().Add(minTimeout).Add(w.perWriteTimeout)
|
||||
if w.server.PerWriteTimeout > 0 {
|
||||
minTimeout := time.Duration(len(p)/1024) * w.server.PerWritePerKbTimeout
|
||||
minDeadline := time.Now().Add(minTimeout).Add(w.server.PerWriteTimeout)
|
||||
|
||||
w.deadline = w.deadline.Add(minTimeout)
|
||||
if minDeadline.After(w.deadline) {
|
||||
@ -266,19 +288,6 @@ func (w *wrappedConn) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Close() error {
|
||||
if atomic.CompareAndSwapInt32(w.closed, 0, 1) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
select {
|
||||
case <-GetManager().IsHammer():
|
||||
// Likely deadlocked request released at hammertime
|
||||
log.Warn("Panic during connection close! %v. Likely there has been a deadlocked request which has been released by forced shutdown.", err)
|
||||
default:
|
||||
log.Error("Panic during connection close! %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
w.server.wg.Done()
|
||||
}
|
||||
w.server.removeConnection(w)
|
||||
return w.Conn.Close()
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ package graceful
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
@ -48,26 +47,8 @@ func (srv *Server) doShutdown() {
|
||||
}
|
||||
|
||||
func (srv *Server) doHammer() {
|
||||
defer func() {
|
||||
// We call srv.wg.Done() until it panics.
|
||||
// This happens if we call Done() when the WaitGroup counter is already at 0
|
||||
// So if it panics -> we're done, Serve() will return and the
|
||||
// parent will goroutine will exit.
|
||||
if r := recover(); r != nil {
|
||||
log.Error("WaitGroup at 0: Error: %v", r)
|
||||
}
|
||||
}()
|
||||
if srv.getState() != stateShuttingDown {
|
||||
return
|
||||
}
|
||||
log.Warn("Forcefully shutting down parent")
|
||||
for {
|
||||
if srv.getState() == stateTerminate {
|
||||
break
|
||||
}
|
||||
srv.wg.Done()
|
||||
|
||||
// Give other goroutines a chance to finish before we forcibly stop them.
|
||||
runtime.Gosched()
|
||||
}
|
||||
srv.closeAllConnections()
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ type ConfigSection interface {
|
||||
HasKey(key string) bool
|
||||
NewKey(name, value string) (ConfigKey, error)
|
||||
Key(key string) ConfigKey
|
||||
DeleteKey(key string)
|
||||
Keys() []ConfigKey
|
||||
ChildSections() []ConfigSection
|
||||
}
|
||||
@ -51,6 +52,7 @@ type ConfigProvider interface {
|
||||
Sections() []ConfigSection
|
||||
NewSection(name string) (ConfigSection, error)
|
||||
GetSection(name string) (ConfigSection, error)
|
||||
DeleteSection(name string)
|
||||
Save() error
|
||||
SaveTo(filename string) error
|
||||
|
||||
@ -168,6 +170,10 @@ func (s *iniConfigSection) Keys() (keys []ConfigKey) {
|
||||
return keys
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) DeleteKey(key string) {
|
||||
s.sec.DeleteKey(key)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
|
||||
for _, s := range s.sec.ChildSections() {
|
||||
sections = append(sections, &iniConfigSection{s})
|
||||
@ -249,6 +255,10 @@ func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) {
|
||||
return &iniConfigSection{sec: sec}, nil
|
||||
}
|
||||
|
||||
func (p *iniConfigProvider) DeleteSection(name string) {
|
||||
p.ini.DeleteSection(name)
|
||||
}
|
||||
|
||||
var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save")
|
||||
|
||||
// Save saves the content into file
|
||||
|
||||
@ -28,7 +28,7 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
act_model "github.com/nektos/act/pkg/model"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@ -38,9 +38,10 @@ const (
|
||||
tplViewActions templates.TplName = "repo/actions/view"
|
||||
)
|
||||
|
||||
type Workflow struct {
|
||||
Entry git.TreeEntry
|
||||
ErrMsg string
|
||||
type WorkflowInfo struct {
|
||||
Entry git.TreeEntry
|
||||
ErrMsg string
|
||||
Workflow *act_model.Workflow
|
||||
}
|
||||
|
||||
// MustEnableActions check if actions are enabled in settings
|
||||
@ -77,7 +78,11 @@ func List(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
workflows := prepareWorkflowDispatchTemplate(ctx, commit)
|
||||
workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@ -112,55 +117,41 @@ func WorkflowDispatchInputs(ctx *context.Context) {
|
||||
ctx.ServerError("GetTagCommit/GetBranchCommit", err)
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, commit)
|
||||
workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplDispatchInputsActions)
|
||||
}
|
||||
|
||||
func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) {
|
||||
workflowID := ctx.FormString("workflow")
|
||||
ctx.Data["CurWorkflow"] = workflowID
|
||||
ctx.Data["CurWorkflowExists"] = false
|
||||
|
||||
var curWorkflow *model.Workflow
|
||||
func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflows []WorkflowInfo, curWorkflowID string) {
|
||||
curWorkflowID = ctx.FormString("workflow")
|
||||
|
||||
_, entries, err := actions.ListWorkflows(commit)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListWorkflows", err)
|
||||
return nil
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// Get all runner labels
|
||||
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsOnline: optional.Some(true),
|
||||
WithAvailable: true,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindRunners", err)
|
||||
return nil
|
||||
}
|
||||
allRunnerLabels := make(container.Set[string])
|
||||
for _, r := range runners {
|
||||
allRunnerLabels.AddMultiple(r.AgentLabels...)
|
||||
}
|
||||
|
||||
workflows = make([]Workflow, 0, len(entries))
|
||||
workflows = make([]WorkflowInfo, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
workflow := Workflow{Entry: *entry}
|
||||
workflow := WorkflowInfo{Entry: *entry}
|
||||
content, err := actions.GetContentFromEntry(entry)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetContentFromEntry", err)
|
||||
return nil
|
||||
return nil, ""
|
||||
}
|
||||
wf, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||
wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
|
||||
if err != nil {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
|
||||
workflows = append(workflows, workflow)
|
||||
continue
|
||||
}
|
||||
workflow.Workflow = wf
|
||||
// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
|
||||
hasJobWithoutNeeds := false
|
||||
// Check whether you have matching runner and a job without "needs"
|
||||
@ -173,22 +164,6 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
|
||||
if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
|
||||
hasJobWithoutNeeds = true
|
||||
}
|
||||
runsOnList := j.RunsOn()
|
||||
for _, ro := range runsOnList {
|
||||
if strings.Contains(ro, "${{") {
|
||||
// Skip if it contains expressions.
|
||||
// The expressions could be very complex and could not be evaluated here,
|
||||
// so just skip it, it's OK since it's just a tooltip message.
|
||||
continue
|
||||
}
|
||||
if !allRunnerLabels.Contains(ro) {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
|
||||
break
|
||||
}
|
||||
}
|
||||
if workflow.ErrMsg != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasJobWithoutNeeds {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
|
||||
@ -197,61 +172,75 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
|
||||
}
|
||||
workflows = append(workflows, workflow)
|
||||
|
||||
if workflow.Entry.Name() == workflowID {
|
||||
curWorkflow = wf
|
||||
ctx.Data["CurWorkflowExists"] = true
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["workflows"] = workflows
|
||||
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
|
||||
|
||||
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
|
||||
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
ctx.Data["ActionsConfig"] = actionsConfig
|
||||
ctx.Data["CurWorkflow"] = curWorkflowID
|
||||
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(curWorkflowID)
|
||||
|
||||
if len(workflowID) > 0 && ctx.Repo.CanWrite(unit.TypeActions) {
|
||||
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
|
||||
isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID)
|
||||
ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled
|
||||
|
||||
if !isWorkflowDisabled && curWorkflow != nil {
|
||||
workflowDispatchConfig := workflowDispatchConfig(curWorkflow)
|
||||
if workflowDispatchConfig != nil {
|
||||
ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig
|
||||
|
||||
branchOpts := git_model.FindBranchOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsDeletedBranch: optional.Some(false),
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
}
|
||||
branches, err := git_model.FindBranchNames(ctx, branchOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindBranchNames", err)
|
||||
return nil
|
||||
}
|
||||
// always put default branch on the top if it exists
|
||||
if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
|
||||
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
|
||||
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
|
||||
}
|
||||
ctx.Data["Branches"] = branches
|
||||
|
||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["Tags"] = tags
|
||||
}
|
||||
}
|
||||
}
|
||||
return workflows
|
||||
return workflows, curWorkflowID
|
||||
}
|
||||
|
||||
func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
||||
func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string) {
|
||||
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
if curWorkflowID == "" || !ctx.Repo.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) {
|
||||
return
|
||||
}
|
||||
|
||||
var curWorkflow *act_model.Workflow
|
||||
for _, workflowInfo := range workflowInfos {
|
||||
if workflowInfo.Entry.Name() == curWorkflowID {
|
||||
if workflowInfo.Workflow == nil {
|
||||
log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID)
|
||||
return
|
||||
}
|
||||
curWorkflow = workflowInfo.Workflow
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if curWorkflow == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["CurWorkflowExists"] = true
|
||||
curWfDispatchCfg := workflowDispatchConfig(curWorkflow)
|
||||
if curWfDispatchCfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["WorkflowDispatchConfig"] = curWfDispatchCfg
|
||||
|
||||
branchOpts := git_model.FindBranchOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsDeletedBranch: optional.Some(false),
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
}
|
||||
branches, err := git_model.FindBranchNames(ctx, branchOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindBranchNames", err)
|
||||
return
|
||||
}
|
||||
// always put default branch on the top
|
||||
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
|
||||
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
|
||||
ctx.Data["Branches"] = branches
|
||||
|
||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Tags"] = tags
|
||||
}
|
||||
|
||||
func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
|
||||
actorID := ctx.FormInt64("actor")
|
||||
status := ctx.FormInt("status")
|
||||
workflowID := ctx.FormString("workflow")
|
||||
@ -302,6 +291,45 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
||||
log.Error("LoadIsRefDeleted", err)
|
||||
}
|
||||
|
||||
// Check for each run if there is at least one online runner that can run its jobs
|
||||
runErrors := make(map[int64]string)
|
||||
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsOnline: optional.Some(true),
|
||||
WithAvailable: true,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindRunners", err)
|
||||
return
|
||||
}
|
||||
for _, run := range runs {
|
||||
if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) {
|
||||
continue
|
||||
}
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunJobsByRunID", err)
|
||||
return
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if !job.Status.IsWaiting() {
|
||||
continue
|
||||
}
|
||||
hasOnlineRunner := false
|
||||
for _, runner := range runners {
|
||||
if runner.CanMatchLabels(job.RunsOn) {
|
||||
hasOnlineRunner = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOnlineRunner {
|
||||
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ","))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Data["RunErrors"] = runErrors
|
||||
|
||||
ctx.Data["Runs"] = runs
|
||||
|
||||
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
|
||||
@ -362,7 +390,7 @@ type WorkflowDispatch struct {
|
||||
Inputs []WorkflowDispatchInput
|
||||
}
|
||||
|
||||
func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
|
||||
func workflowDispatchConfig(w *act_model.Workflow) *WorkflowDispatch {
|
||||
switch w.RawOn.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var val string
|
||||
|
||||
@ -7,14 +7,14 @@
|
||||
{{if .HasWorkflowsOrRuns}}
|
||||
<div class="ui stackable grid">
|
||||
<div class="four wide column">
|
||||
<div class="ui fluid vertical menu">
|
||||
<a class="item{{if not $.CurWorkflow}} active{{end}}" href="?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
|
||||
<div class="ui fluid vertical menu flex-items-block">
|
||||
<a class="item {{if not $.CurWorkflow}}active{{end}}" href="?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a>
|
||||
{{range .workflows}}
|
||||
<a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}}
|
||||
<a class="item {{if eq .Entry.Name $.CurWorkflow}}active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">
|
||||
<span class="gt-ellipsis">{{.Entry.Name}}</span>
|
||||
|
||||
{{if .ErrMsg}}
|
||||
<span data-tooltip-content="{{.ErrMsg}}">
|
||||
{{svg "octicon-alert" 16 "text red"}}
|
||||
</span>
|
||||
<span class="flex-text-inline" data-tooltip-content="{{.ErrMsg}}">{{svg "octicon-alert" 16 "text red"}}</span>
|
||||
{{end}}
|
||||
|
||||
{{if $.ActionsConfig.IsWorkflowDisabled .Entry.Name}}
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
</a>
|
||||
<div class="flex-item-body">
|
||||
<span><b>{{if not $.CurWorkflow}}{{$run.WorkflowID}} {{end}}#{{$run.Index}}</b>:</span>
|
||||
|
||||
{{- if $run.ScheduleID -}}
|
||||
{{ctx.Locale.Tr "actions.runs.scheduled"}}
|
||||
{{- else -}}
|
||||
@ -24,6 +25,13 @@
|
||||
{{ctx.Locale.Tr "actions.runs.pushed_by"}}
|
||||
<a href="{{$run.TriggerUser.HomeLink}}">{{$run.TriggerUser.GetDisplayName}}</a>
|
||||
{{- end -}}
|
||||
|
||||
{{$errMsg := index $.RunErrors $run.ID}}
|
||||
{{if $errMsg}}
|
||||
<span class="flex-text-inline" data-tooltip-content="{{$errMsg}}">
|
||||
{{svg "octicon-alert" 16 "text red"}}
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
|
||||
@ -1104,6 +1104,7 @@ table th[data-sortt-desc] .svg {
|
||||
}
|
||||
|
||||
.ui.list.flex-items-block > .item,
|
||||
.ui.vertical.menu.flex-items-block > .item,
|
||||
.ui.form .field > label.flex-text-block, /* override fomantic "block" style */
|
||||
.flex-items-block > .item,
|
||||
.flex-text-block {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user