From 9c72bc8977359ef5b79996d2b9bd41882438d11d Mon Sep 17 00:00:00 2001 From: James Elliott Date: Fri, 16 Sep 2022 14:21:05 +1000 Subject: [PATCH] ci: gen github tmpl locales and commitlint (#3759) This adds several automatic generators for Authelia docs etc. --- .github/ISSUE_TEMPLATE/bug-report.yml | 138 +++++++++ .github/ISSUE_TEMPLATE/bug_report.md | 41 --- .github/ISSUE_TEMPLATE/config.yml | 9 + .github/ISSUE_TEMPLATE/feature-request.yml | 47 ++++ .github/ISSUE_TEMPLATE/feature_request.md | 26 -- .github/ISSUE_TEMPLATE/misc.md | 7 - .yamllint.yml | 3 +- cmd/authelia-gen/cmd_all.go | 36 --- cmd/authelia-gen/cmd_code.go | 263 +++++++++++++++++- cmd/authelia-gen/cmd_code_keys.go | 173 ------------ cmd/authelia-gen/cmd_commit_msg.go | 220 +++++++++++++++ cmd/authelia-gen/cmd_docs.go | 22 +- cmd/authelia-gen/cmd_docs_cli.go | 38 ++- cmd/authelia-gen/cmd_docs_date.go | 21 +- cmd/authelia-gen/cmd_docs_keys.go | 81 ++++++ cmd/authelia-gen/cmd_github.go | 236 ++++++++++++++++ cmd/authelia-gen/cmd_locales.go | 215 ++++++++++++++ cmd/authelia-gen/cmd_root.go | 124 +++++++++ cmd/authelia-gen/const.go | 80 +++++- cmd/authelia-gen/main.go | 24 +- cmd/authelia-gen/templates.go | 69 +++++ .../cmd-authelia-scripts-gen.go.tmpl | 11 + ...contributing-development-commitmsg.md.tmpl | 138 +++++++++ .../templates/dot_commitlintrc.js.tmpl | 25 ++ .../github_issue_template_bug_report.yml.tmpl | 108 +++++++ .../github_issue_template_feature.yml.tmpl | 47 ++++ ...nternal_configuration_schema_keys.go.tmpl} | 0 .../templates/web_i18n_index.ts.tmpl | 47 ++++ cmd/authelia-gen/types.go | 123 ++++++++ cmd/authelia-scripts/cmd/build.go | 7 +- cmd/authelia-scripts/cmd/gen.go | 11 + docs/config/_default/menus/menus.en.toml | 12 +- .../en/configuration/methods/environment.md | 4 + .../en/configuration/methods/secrets.md | 18 +- .../development/guidelines-commit-message.md | 13 +- .../privacy-policy.md => policies/privacy.md} | 2 +- .../en/{information => policies}/security.md | 7 +- .../cli/authelia-gen/authelia-gen.md | 28 +- .../cli/authelia-gen/authelia-gen_all.md | 32 --- .../cli/authelia-gen/authelia-gen_code.md | 24 ++ .../authelia-gen/authelia-gen_code_keys.md | 27 +- .../authelia-gen/authelia-gen_code_scripts.md | 55 ++++ .../authelia-gen/authelia-gen_commit-lint.md | 55 ++++ .../cli/authelia-gen/authelia-gen_docs.md | 27 +- .../cli/authelia-gen/authelia-gen_docs_cli.md | 22 +- .../authelia-gen/authelia-gen_docs_date.md | 22 +- .../authelia-gen/authelia-gen_docs_keys.md | 55 ++++ .../authelia-gen/authelia-gen_docs_time.md | 42 --- .../cli/authelia-gen/authelia-gen_github.md | 56 ++++ .../authelia-gen_github_issue-templates.md | 57 ++++ ...a-gen_github_issue-templates_bug-report.md | 55 ++++ ..._github_issue-templates_feature-request.md | 55 ++++ .../cli/authelia-gen/authelia-gen_locales.md | 55 ++++ .../authelia-scripts_certificates.md | 39 --- .../authelia-scripts_certificates_generate.md | 49 ---- .../authelia-scripts_hash-password.md | 49 ---- .../authelia-scripts/authelia-scripts_rsa.md | 38 --- .../authelia-scripts_rsa_generate.md | 43 --- .../reference/guides/domain-sanitizaiton.md | 30 -- .../reference/guides/internationalization.md | 17 ++ .../guides/troubleshooting-sanitizaiton.md | 43 +++ docs/data/configkeys.json | 1 + docs/data/languages.json | 2 +- docs/layouts/policies/list.html | 22 ++ .../layouts/shortcodes/table-config-keys.html | 14 + .../shortcodes/table-i18n-locales.html | 5 + docs/package.json | 2 +- internal/configuration/helpers.go | 28 +- internal/configuration/helpers_test.go | 12 +- internal/configuration/sources.go | 10 +- web/src/i18n/index.ts | 6 + 71 files changed, 2770 insertions(+), 753 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/ISSUE_TEMPLATE/misc.md delete mode 100644 cmd/authelia-gen/cmd_all.go delete mode 100644 cmd/authelia-gen/cmd_code_keys.go create mode 100644 cmd/authelia-gen/cmd_commit_msg.go create mode 100644 cmd/authelia-gen/cmd_docs_keys.go create mode 100644 cmd/authelia-gen/cmd_github.go create mode 100644 cmd/authelia-gen/cmd_locales.go create mode 100644 cmd/authelia-gen/cmd_root.go create mode 100644 cmd/authelia-gen/templates.go create mode 100644 cmd/authelia-gen/templates/cmd-authelia-scripts-gen.go.tmpl create mode 100644 cmd/authelia-gen/templates/docs-contributing-development-commitmsg.md.tmpl create mode 100644 cmd/authelia-gen/templates/dot_commitlintrc.js.tmpl create mode 100644 cmd/authelia-gen/templates/github_issue_template_bug_report.yml.tmpl create mode 100644 cmd/authelia-gen/templates/github_issue_template_feature.yml.tmpl rename cmd/authelia-gen/templates/{config_keys.go.tmpl => internal_configuration_schema_keys.go.tmpl} (100%) create mode 100644 cmd/authelia-gen/templates/web_i18n_index.ts.tmpl create mode 100644 cmd/authelia-gen/types.go create mode 100644 cmd/authelia-scripts/cmd/gen.go rename docs/content/en/{information/privacy-policy.md => policies/privacy.md} (92%) rename docs/content/en/{information => policies}/security.md (93%) delete mode 100644 docs/content/en/reference/cli/authelia-gen/authelia-gen_all.md create mode 100644 docs/content/en/reference/cli/authelia-gen/authelia-gen_code_scripts.md create mode 100644 docs/content/en/reference/cli/authelia-gen/authelia-gen_commit-lint.md create mode 100644 docs/content/en/reference/cli/authelia-gen/authelia-gen_docs_keys.md delete mode 100644 docs/content/en/reference/cli/authelia-gen/authelia-gen_docs_time.md create mode 100644 docs/content/en/reference/cli/authelia-gen/authelia-gen_github.md create mode 100644 docs/content/en/reference/cli/authelia-gen/authelia-gen_github_issue-templates.md create mode 100644 docs/content/en/reference/cli/authelia-gen/authelia-gen_github_issue-templates_bug-report.md create mode 100644 docs/content/en/reference/cli/authelia-gen/authelia-gen_github_issue-templates_feature-request.md create mode 100644 docs/content/en/reference/cli/authelia-gen/authelia-gen_locales.md delete mode 100644 docs/content/en/reference/cli/authelia-scripts/authelia-scripts_certificates.md delete mode 100644 docs/content/en/reference/cli/authelia-scripts/authelia-scripts_certificates_generate.md delete mode 100644 docs/content/en/reference/cli/authelia-scripts/authelia-scripts_hash-password.md delete mode 100644 docs/content/en/reference/cli/authelia-scripts/authelia-scripts_rsa.md delete mode 100644 docs/content/en/reference/cli/authelia-scripts/authelia-scripts_rsa_generate.md delete mode 100644 docs/content/en/reference/guides/domain-sanitizaiton.md create mode 100644 docs/content/en/reference/guides/internationalization.md create mode 100644 docs/content/en/reference/guides/troubleshooting-sanitizaiton.md create mode 100644 docs/data/configkeys.json create mode 100644 docs/layouts/policies/list.html create mode 100644 docs/layouts/shortcodes/table-config-keys.html create mode 100644 docs/layouts/shortcodes/table-i18n-locales.html diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000..3b107f758 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,138 @@ +--- +name: Bug Report +description: Report a bug +labels: + - type/bug/unconfirmed + - status/needs-triage + - priority/4-normal +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report. If you are unsure if this is actually a bug we generally recommend creating a [Question and Answer Discussion](https://github.com/authelia/authelia/discussions/new?category=q-a) first. + + Please review the following requirements before submitting this issue type: + + 1. Please ensure you do not report security vulnerabilities via this method. See our [Security Policy](https://www.authelia.com/security-policy). + 2. Please try to give as much information as possible for us to be able to reproduce the issue and provide a quick fix. + 3. Please ensure an issue does not already exist for this potential bug. + 4. Please only provide specific versions. Latest is not a version. + 5. Please read the [Troubleshooting Sanitization](https://www.authelia.com/r/sanitize) reference guide if you plan on removing or adjusting any values for the logs or configuration files + - type: dropdown + id: version + attributes: + label: Version + description: What version(s) of Authelia can you reproduce this bug on? + multiple: true + options: + - v4.36.7 + - v4.36.6 + - v4.36.5 + - v4.36.4 + - v4.36.3 + - v4.36.2 + - v4.36.1 + - v4.36.0 + - v4.35.6 + - v4.35.5 + - v4.35.4 + - v4.35.3 + - v4.35.2 + - v4.35.1 + - v4.35.0 + - v4.34.6 + - v4.34.5 + - v4.34.4 + - v4.34.3 + - v4.34.2 + - v4.34.1 + - v4.34.0 + - v4.33.2 + - v4.33.1 + - v4.33.0 + - v4.32.2 + - v4.32.1 + - v4.32.0 + - v4.31.0 + validations: + required: true + - type: dropdown + id: deployment + attributes: + label: Deployment Method + description: How are you deploying Authelia? + options: + - Docker + - Kubernetes + - Bare-metal + - Other + validations: + required: true + - type: dropdown + id: proxy + attributes: + label: Reverse Proxy + description: What reverse proxy are you using? + options: + - Caddy + - Traefik + - Envoy + - NGINX + - SWAG + - NGINX Proxy Manager + - HAProxy + validations: + required: true + - type: input + id: proxy-version + attributes: + label: Reverse Proxy Version + description: What is the version of your reverse proxy? + placeholder: x.x.x + validations: + required: false + - type: textarea + id: description + attributes: + label: Description + description: Describe the bug + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Describe how we can reproduce this issue + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs + description: Provide the logs (the template will automatically put this content in a code block) + render: shell + validations: + required: false + - type: textarea + id: configuration + attributes: + label: Configuration + description: Provide the Authelia configuration file (the template will automatically put this content in a code block) + render: yaml + validations: + required: false + - type: textarea + id: expectations + attributes: + label: Expectations + description: Describe the desired or expected results + validations: + required: false + - type: textarea + id: documentation + attributes: + label: Documentation + description: Provide any relevant specification or other documentation if applicable + validations: + required: false +... diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 76a60c8f8..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: Bug Report -about: Use this template to report bugs other than security vulnerabilities -labels: Possible Bug ---- -## Bug Report - - -### Description - - - -### Expected Behaviour - -_N/A_ - - -### Reproduction Steps - -_N/A_ - - -### Additional Information - -_N/A_ - \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 899f94db9..2a89a14ac 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,15 @@ --- blank_issues_enabled: false contact_links: + - name: Idea + url: https://github.com/authelia/authelia/discussions/new?category=ideas + about: Submit an Idea for Voting + - name: Question + url: https://github.com/authelia/authelia/discussions/new?category=q-a + about: Ask a Question + - name: Discussion + url: https://github.com/authelia/authelia/discussions/new + about: Start a Discussion related to Ideas, Polls, Show and Tell, or General Topics - name: Documentation url: https://www.authelia.com/ about: Read the Documentation diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 000000000..00a448c06 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,47 @@ +--- +name: Feature Request +description: Submit a Feature for Design which Has Been Submitted as an Idea and has been voted on +labels: + - type/feature + - status/needs-design + - priority/4-normal +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request. A feature request is created as issue for the purpose of tracking the design and implementation of a feature. + + Please review the following requirements before submitting this issue type: + + 1. Ensure there are no other similar feature requests. + 2. Make sure you've checked the [Documentation](https://www.authelia.com) doesn't clearly document the features existence already. + 3. Consider creating an [Idea Discussion](https://github.com/authelia/authelia/discussions/new?category=ideas) which can be voted on instead if one doesn't exist. + - type: textarea + id: description + attributes: + label: Description + description: Describe the feature + validations: + required: true + - type: textarea + id: use-case + attributes: + label: Use Case + description: Provide a use case + validations: + required: true + - type: textarea + id: details + attributes: + label: Details + description: Describe the feature in detail + validations: + required: false + - type: textarea + id: documentation + attributes: + label: Documentation + description: Provide any relevant specification or other documentation if applicable + validations: + required: false +... diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 4354ccba9..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Feature Request -about: Use this template to request features -labels: Feature Request ---- -## Feature Request - - -### Description - - - -### Use Case - -_N/A_ - \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/misc.md b/.github/ISSUE_TEMPLATE/misc.md deleted file mode 100644 index 23a941098..000000000 --- a/.github/ISSUE_TEMPLATE/misc.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: Miscellaneous -about: Use this template for everything other than feature requests, security vulnerabilities, or bug reports such as questions ---- - \ No newline at end of file diff --git a/.yamllint.yml b/.yamllint.yml index 3d31542b4..d7fc7209f 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -6,7 +6,8 @@ ignore: | internal/configuration/test_resources/config_bad_quoting.yml web/pnpm-lock.yaml web/node_modules/ - + .github/ISSUE_TEMPLATE/feature-request.yml + .github/ISSUE_TEMPLATE/bug-report.yml rules: document-end: level: warning diff --git a/cmd/authelia-gen/cmd_all.go b/cmd/authelia-gen/cmd_all.go deleted file mode 100644 index aba00a5a3..000000000 --- a/cmd/authelia-gen/cmd_all.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "github.com/spf13/cobra" -) - -func newAllCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "all", - Short: "Run all generators with default options", - RunE: allRunE, - - DisableAutoGenTag: true, - } - - return cmd -} - -func allRunE(cmd *cobra.Command, args []string) (err error) { - for _, subCmd := range cmd.Parent().Commands() { - if subCmd == cmd || subCmd.Use == "completion" || subCmd.Use == "help [command]" { - continue - } - - switch { - case subCmd.RunE != nil: - if err = subCmd.RunE(subCmd, args); err != nil { - return err - } - case subCmd.Run != nil: - subCmd.Run(subCmd, args) - } - } - - return nil -} diff --git a/cmd/authelia-gen/cmd_code.go b/cmd/authelia-gen/cmd_code.go index f0eea6b02..f0cac4bba 100644 --- a/cmd/authelia-gen/cmd_code.go +++ b/cmd/authelia-gen/cmd_code.go @@ -1,34 +1,271 @@ package main import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/mail" + "net/url" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + "time" + "github.com/spf13/cobra" + + "github.com/authelia/authelia/v4/internal/configuration/schema" ) func newCodeCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "code", + Use: cmdUseCode, Short: "Generate code", - RunE: codeRunE, + RunE: rootSubCommandsRunE, DisableAutoGenTag: true, } - cmd.AddCommand(newCodeKeysCmd()) + cmd.AddCommand(newCodeKeysCmd(), newCodeScriptsCmd()) return cmd } -func codeRunE(cmd *cobra.Command, args []string) (err error) { - for _, subCmd := range cmd.Commands() { - switch { - case subCmd.RunE != nil: - if err = subCmd.RunE(subCmd, args); err != nil { - return err - } - case subCmd.Run != nil: - subCmd.Run(subCmd, args) - } +func newCodeScriptsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseCodeScripts, + Short: "Generate the generated portion of the authelia-scripts command", + RunE: codeScriptsRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func newCodeKeysCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseKeys, + Short: "Generate the list of valid configuration keys", + RunE: codeKeysRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func codeScriptsRunE(cmd *cobra.Command, args []string) (err error) { + var ( + root, pathScriptsGen string + resp *http.Response + ) + + data := &tmplScriptsGEnData{} + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if pathScriptsGen, err = cmd.Flags().GetString(cmdFlagFileScriptsGen); err != nil { + return err + } + + if data.Package, err = cmd.Flags().GetString(cmdFlagPackageScriptsGen); err != nil { + return err + } + + if resp, err = http.Get("https://api.github.com/repos/swagger-api/swagger-ui/tags"); err != nil { + return fmt.Errorf("failed to get latest version of the Swagger UI: %w", err) + } + + defer resp.Body.Close() + + var ( + respJSON []GitHubTagsJSON + respRaw []byte + ) + + if respRaw, err = io.ReadAll(resp.Body); err != nil { + return fmt.Errorf("failed to get latest version of the Swagger UI: %w", err) + } + + if err = json.Unmarshal(respRaw, &respJSON); err != nil { + return fmt.Errorf("failed to get latest version of the Swagger UI: %w", err) + } + + if len(respJSON) < 1 { + return fmt.Errorf("failed to get latest version of the Swagger UI: the api returned zero results") + } + + if strings.HasPrefix(respJSON[0].Name, "v") { + data.VersionSwaggerUI = respJSON[0].Name[1:] + } else { + data.VersionSwaggerUI = respJSON[0].Name + } + + fullPathScriptsGen := filepath.Join(root, pathScriptsGen) + + var f *os.File + + if f, err = os.Create(fullPathScriptsGen); err != nil { + return fmt.Errorf("failed to create file '%s': %w", fullPathScriptsGen, err) + } + + if err = tmplScriptsGen.Execute(f, data); err != nil { + _ = f.Close() + + return fmt.Errorf("failed to write output file '%s': %w", fullPathScriptsGen, err) + } + + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close output file '%s': %w", fullPathScriptsGen, err) } return nil } + +// GitHubTagsJSON represents the JSON struct for the GitHub Tags API. +type GitHubTagsJSON struct { + Name string `json:"name"` +} + +func codeKeysRunE(cmd *cobra.Command, args []string) (err error) { + var ( + pathCodeConfigKeys, root string + + f *os.File + ) + + data := tmplConfigurationKeysData{ + Timestamp: time.Now(), + Keys: readTags("", reflect.TypeOf(schema.Configuration{})), + } + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if pathCodeConfigKeys, err = cmd.Flags().GetString(cmdFlagFileConfigKeys); err != nil { + return err + } + + if data.Package, err = cmd.Flags().GetString(cmdFlagPackageConfigKeys); err != nil { + return err + } + + fullPathCodeConfigKeys := filepath.Join(root, pathCodeConfigKeys) + + if f, err = os.Create(fullPathCodeConfigKeys); err != nil { + return fmt.Errorf("failed to create file '%s': %w", fullPathCodeConfigKeys, err) + } + + if err = tmplCodeConfigurationSchemaKeys.Execute(f, data); err != nil { + _ = f.Close() + + return fmt.Errorf("failed to write output file '%s': %w", fullPathCodeConfigKeys, err) + } + + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close output file '%s': %w", fullPathCodeConfigKeys, err) + } + + return nil +} + +var decodedTypes = []reflect.Type{ + reflect.TypeOf(mail.Address{}), + reflect.TypeOf(regexp.Regexp{}), + reflect.TypeOf(url.URL{}), + reflect.TypeOf(time.Duration(0)), + reflect.TypeOf(schema.Address{}), +} + +func containsType(needle reflect.Type, haystack []reflect.Type) (contains bool) { + for _, t := range haystack { + if needle.Kind() == reflect.Ptr { + if needle.Elem() == t { + return true + } + } else if needle == t { + return true + } + } + + return false +} + +func readTags(prefix string, t reflect.Type) (tags []string) { + tags = make([]string, 0) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + tag := field.Tag.Get("koanf") + + if tag == "" { + tags = append(tags, prefix) + + continue + } + + switch field.Type.Kind() { + case reflect.Struct: + if !containsType(field.Type, decodedTypes) { + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type)...) + + continue + } + case reflect.Slice: + if field.Type.Elem().Kind() == reflect.Struct { + if !containsType(field.Type.Elem(), decodedTypes) { + tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false)) + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) + + continue + } + } + case reflect.Ptr: + switch field.Type.Elem().Kind() { + case reflect.Struct: + if !containsType(field.Type.Elem(), decodedTypes) { + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type.Elem())...) + + continue + } + case reflect.Slice: + if field.Type.Elem().Elem().Kind() == reflect.Struct { + if !containsType(field.Type.Elem(), decodedTypes) { + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) + + continue + } + } + } + } + + tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false)) + } + + return tags +} + +func getKeyNameFromTagAndPrefix(prefix, name string, slice bool) string { + nameParts := strings.SplitN(name, ",", 2) + + if prefix == "" { + return nameParts[0] + } + + if len(nameParts) == 2 && nameParts[1] == "squash" { + return prefix + } + + if slice { + return fmt.Sprintf("%s.%s[]", prefix, nameParts[0]) + } + + return fmt.Sprintf("%s.%s", prefix, nameParts[0]) +} diff --git a/cmd/authelia-gen/cmd_code_keys.go b/cmd/authelia-gen/cmd_code_keys.go deleted file mode 100644 index 4599ae148..000000000 --- a/cmd/authelia-gen/cmd_code_keys.go +++ /dev/null @@ -1,173 +0,0 @@ -package main - -import ( - "fmt" - "net/mail" - "net/url" - "os" - "reflect" - "regexp" - "strings" - "text/template" - "time" - - "github.com/spf13/cobra" - - "github.com/authelia/authelia/v4/internal/configuration/schema" -) - -func newCodeKeysCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "keys", - Short: "Generate the list of valid configuration keys", - RunE: codeKeysRunE, - - DisableAutoGenTag: true, - } - - cmd.Flags().StringP("file", "f", "./internal/configuration/schema/keys.go", "Sets the path of the keys file") - cmd.Flags().String("package", "schema", "Sets the package name of the keys file") - - return cmd -} - -func codeKeysRunE(cmd *cobra.Command, args []string) (err error) { - var ( - file string - - f *os.File - ) - - data := keysTemplateStruct{ - Timestamp: time.Now(), - Keys: readTags("", reflect.TypeOf(schema.Configuration{})), - } - - if file, err = cmd.Flags().GetString("file"); err != nil { - return err - } - - if data.Package, err = cmd.Flags().GetString("package"); err != nil { - return err - } - - if f, err = os.Create(file); err != nil { - return fmt.Errorf("failed to create file '%s': %w", file, err) - } - - var ( - content []byte - tmpl *template.Template - ) - - if content, err = templatesFS.ReadFile("templates/config_keys.go.tmpl"); err != nil { - return err - } - - if tmpl, err = template.New("keys").Parse(string(content)); err != nil { - return err - } - - return tmpl.Execute(f, data) -} - -type keysTemplateStruct struct { - Timestamp time.Time - Keys []string - Package string -} - -var decodedTypes = []reflect.Type{ - reflect.TypeOf(mail.Address{}), - reflect.TypeOf(regexp.Regexp{}), - reflect.TypeOf(url.URL{}), - reflect.TypeOf(time.Duration(0)), - reflect.TypeOf(schema.Address{}), -} - -func containsType(needle reflect.Type, haystack []reflect.Type) (contains bool) { - for _, t := range haystack { - if needle.Kind() == reflect.Ptr { - if needle.Elem() == t { - return true - } - } else if needle == t { - return true - } - } - - return false -} - -func readTags(prefix string, t reflect.Type) (tags []string) { - tags = make([]string, 0) - - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - - tag := field.Tag.Get("koanf") - - if tag == "" { - tags = append(tags, prefix) - - continue - } - - switch field.Type.Kind() { - case reflect.Struct: - if !containsType(field.Type, decodedTypes) { - tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type)...) - - continue - } - case reflect.Slice: - if field.Type.Elem().Kind() == reflect.Struct { - if !containsType(field.Type.Elem(), decodedTypes) { - tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false)) - tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) - - continue - } - } - case reflect.Ptr: - switch field.Type.Elem().Kind() { - case reflect.Struct: - if !containsType(field.Type.Elem(), decodedTypes) { - tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type.Elem())...) - - continue - } - case reflect.Slice: - if field.Type.Elem().Elem().Kind() == reflect.Struct { - if !containsType(field.Type.Elem(), decodedTypes) { - tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) - - continue - } - } - } - } - - tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false)) - } - - return tags -} - -func getKeyNameFromTagAndPrefix(prefix, name string, slice bool) string { - nameParts := strings.SplitN(name, ",", 2) - - if prefix == "" { - return nameParts[0] - } - - if len(nameParts) == 2 && nameParts[1] == "squash" { - return prefix - } - - if slice { - return fmt.Sprintf("%s.%s[]", prefix, nameParts[0]) - } - - return fmt.Sprintf("%s.%s", prefix, nameParts[0]) -} diff --git a/cmd/authelia-gen/cmd_commit_msg.go b/cmd/authelia-gen/cmd_commit_msg.go new file mode 100644 index 000000000..cadf70d68 --- /dev/null +++ b/cmd/authelia-gen/cmd_commit_msg.go @@ -0,0 +1,220 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/spf13/cobra" +) + +// CommitMessageTmpl is a template data structure which is used to generate files with commit message information. +type CommitMessageTmpl struct { + Scopes ScopesTmpl + Types TypesTmpl +} + +// TypesTmpl is a template data structure which is used to generate files with commit message types. +type TypesTmpl struct { + List []string + Details []NameDescriptionTmpl +} + +// ScopesTmpl is a template data structure which is used to generate files with commit message scopes. +type ScopesTmpl struct { + All []string + Packages []string + Extra []NameDescriptionTmpl +} + +// NameDescriptionTmpl is a template item which includes a name, description and list of scopes. +type NameDescriptionTmpl struct { + Name string + Description string + Scopes []string +} + +func newCommitLintCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseCommitLint, + Short: "Generate commit lint files", + RunE: commitLintRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +var commitScopesExtra = []NameDescriptionTmpl{ + {"api", "used for changes that change the openapi specification", nil}, + {"cmd", "used for changes to the `%s` top level binaries", nil}, + {"web", "used for changes to the React based frontend", nil}, +} + +var commitTypes = []NameDescriptionTmpl{ + {"build", "Changes that affect the build system or external dependencies", []string{"bundler", "deps", "docker", "go", "npm"}}, + {"ci", "Changes to our CI configuration files and scripts", []string{"autheliabot", "buildkite", "codecov", "golangci-lint", "renovate", "reviewdog"}}, + {"docs", "Documentation only changes", nil}, + {"feat", "A new feature", nil}, + {"fix", "A bug fix", nil}, + {"i18n", "Updating translations or internationalization settings", nil}, + {"perf", "A code change that improves performance", nil}, + {"refactor", "A code change that neither fixes a bug nor adds a feature", nil}, + {"release", "Releasing a new version of Authelia", nil}, + {"test", "Adding missing tests or correcting existing tests", nil}, +} + +var commitTypesExtra = []string{"revert"} + +func getGoPackages(dir string) (pkgs []string, err error) { + var ( + entries []os.DirEntry + entriesSub []os.DirEntry + ) + + if entries, err = os.ReadDir(dir); err != nil { + return nil, fmt.Errorf("failed to detect go packages in directory '%s': %w", dir, err) + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + if entriesSub, err = os.ReadDir(filepath.Join(dir, entry.Name())); err != nil { + continue + } + + for _, entrySub := range entriesSub { + if entrySub.IsDir() { + continue + } + + if strings.HasSuffix(entrySub.Name(), ".go") { + pkgs = append(pkgs, entry.Name()) + break + } + } + } + + return pkgs, nil +} + +func commitLintRunE(cmd *cobra.Command, args []string) (err error) { + var root, pathCommitLintConfig, pathDocsCommitMessageGuidelines string + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if pathCommitLintConfig, err = cmd.Flags().GetString(cmdFlagFileConfigCommitLint); err != nil { + return err + } + + if pathDocsCommitMessageGuidelines, err = cmd.Flags().GetString(cmdFlagFileDocsCommitMsgGuidelines); err != nil { + return err + } + + data := &CommitMessageTmpl{ + Scopes: ScopesTmpl{ + All: []string{}, + Packages: []string{}, + Extra: []NameDescriptionTmpl{}, + }, + Types: TypesTmpl{ + List: []string{}, + Details: []NameDescriptionTmpl{}, + }, + } + + var ( + cmds []string + pkgs []string + ) + + if cmds, err = getGoPackages(filepath.Join(root, subPathCmd)); err != nil { + return err + } + + if pkgs, err = getGoPackages(filepath.Join(root, subPathInternal)); err != nil { + return err + } + + data.Scopes.All = append(data.Scopes.All, pkgs...) + data.Scopes.Packages = append(data.Scopes.Packages, pkgs...) + + for _, scope := range commitScopesExtra { + switch scope.Name { + case subPathCmd: + data.Scopes.Extra = append(data.Scopes.Extra, NameDescriptionTmpl{Name: scope.Name, Description: fmt.Sprintf(scope.Description, strings.Join(cmds, "|"))}) + default: + data.Scopes.Extra = append(data.Scopes.Extra, scope) + } + + data.Scopes.All = append(data.Scopes.All, scope.Name) + } + + for _, cType := range commitTypes { + data.Types.List = append(data.Types.List, cType.Name) + data.Types.Details = append(data.Types.Details, cType) + + data.Scopes.All = append(data.Scopes.All, cType.Scopes...) + } + + data.Types.List = append(data.Types.List, commitTypesExtra...) + + sort.Slice(data.Scopes.All, func(i, j int) bool { + return data.Scopes.All[i] < data.Scopes.All[j] + }) + + sort.Slice(data.Scopes.Packages, func(i, j int) bool { + return data.Scopes.Packages[i] < data.Scopes.Packages[j] + }) + + sort.Slice(data.Scopes.Extra, func(i, j int) bool { + return data.Scopes.Extra[i].Name < data.Scopes.Extra[j].Name + }) + + sort.Slice(data.Types.List, func(i, j int) bool { + return data.Types.List[i] < data.Types.List[j] + }) + + sort.Slice(data.Types.Details, func(i, j int) bool { + return data.Types.Details[i].Name < data.Types.Details[j].Name + }) + + var f *os.File + + fullPathCommitLintConfig := filepath.Join(root, pathCommitLintConfig) + + if f, err = os.Create(fullPathCommitLintConfig); err != nil { + return fmt.Errorf("failed to create output file '%s': %w", fullPathCommitLintConfig, err) + } + + if err = tmplDotCommitLintRC.Execute(f, data); err != nil { + return fmt.Errorf("failed to write output file '%s': %w", fullPathCommitLintConfig, err) + } + + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close output file '%s': %w", fullPathCommitLintConfig, err) + } + + fullPathDocsCommitMessageGuidelines := filepath.Join(root, pathDocsCommitMessageGuidelines) + + if f, err = os.Create(fullPathDocsCommitMessageGuidelines); err != nil { + return fmt.Errorf("failed to create output file '%s': %w", fullPathDocsCommitMessageGuidelines, err) + } + + if err = tmplDocsCommitMessageGuidelines.Execute(f, data); err != nil { + return fmt.Errorf("failed to write output file '%s': %w", fullPathDocsCommitMessageGuidelines, err) + } + + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close output file '%s': %w", fullPathDocsCommitMessageGuidelines, err) + } + + return nil +} diff --git a/cmd/authelia-gen/cmd_docs.go b/cmd/authelia-gen/cmd_docs.go index 8fa4b26b6..2902b8aa0 100644 --- a/cmd/authelia-gen/cmd_docs.go +++ b/cmd/authelia-gen/cmd_docs.go @@ -6,30 +6,14 @@ import ( func newDocsCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "docs", + Use: cmdUseDocs, Short: "Generate docs", - RunE: docsRunE, + RunE: rootSubCommandsRunE, DisableAutoGenTag: true, } - cmd.PersistentFlags().StringP("cwd", "C", "", "Sets the CWD for git commands") - cmd.AddCommand(newDocsCLICmd(), newDocsDateCmd()) + cmd.AddCommand(newDocsCLICmd(), newDocsDateCmd(), newDocsKeysCmd()) return cmd } - -func docsRunE(cmd *cobra.Command, args []string) (err error) { - for _, subCmd := range cmd.Commands() { - switch { - case subCmd.RunE != nil: - if err = subCmd.RunE(subCmd, args); err != nil { - return err - } - case subCmd.Run != nil: - subCmd.Run(subCmd, args) - } - } - - return nil -} diff --git a/cmd/authelia-gen/cmd_docs_cli.go b/cmd/authelia-gen/cmd_docs_cli.go index 5e0c01bb7..e41b6f189 100644 --- a/cmd/authelia-gen/cmd_docs_cli.go +++ b/cmd/authelia-gen/cmd_docs_cli.go @@ -16,52 +16,56 @@ import ( func newDocsCLICmd() *cobra.Command { cmd := &cobra.Command{ - Use: "cli", + Use: cmdUseDocsCLI, Short: "Generate CLI docs", RunE: docsCLIRunE, DisableAutoGenTag: true, } - cmd.Flags().StringP("directory", "d", "./docs/content/en/reference/cli", "The directory to store the markdown in") - return cmd } func docsCLIRunE(cmd *cobra.Command, args []string) (err error) { - var root string + var root, pathDocsCLIReference string - if root, err = cmd.Flags().GetString("directory"); err != nil { + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { return err } - if err = os.MkdirAll(root, 0775); err != nil { + if pathDocsCLIReference, err = cmd.Flags().GetString(cmdFlagDocsCLIReference); err != nil { + return err + } + + fullPathDocsCLIReference := filepath.Join(root, pathDocsCLIReference) + + if err = os.MkdirAll(fullPathDocsCLIReference, 0775); err != nil { if !os.IsExist(err) { return err } } - if err = genCLIDoc(commands.NewRootCmd(), filepath.Join(root, "authelia")); err != nil { + if err = genCLIDoc(commands.NewRootCmd(), filepath.Join(fullPathDocsCLIReference, "authelia")); err != nil { return err } - if err = genCLIDocWriteIndex(root, "authelia"); err != nil { + if err = genCLIDocWriteIndex(fullPathDocsCLIReference, "authelia"); err != nil { return err } - if err = genCLIDoc(cmdscripts.NewRootCmd(), filepath.Join(root, "authelia-scripts")); err != nil { + if err = genCLIDoc(cmdscripts.NewRootCmd(), filepath.Join(fullPathDocsCLIReference, "authelia-scripts")); err != nil { return err } - if err = genCLIDocWriteIndex(root, "authelia-scripts"); err != nil { + if err = genCLIDocWriteIndex(fullPathDocsCLIReference, "authelia-scripts"); err != nil { return err } - if err = genCLIDoc(newRootCmd(), filepath.Join(root, "authelia-gen")); err != nil { + if err = genCLIDoc(newRootCmd(), filepath.Join(fullPathDocsCLIReference, cmdUseRoot)); err != nil { return err } - if err = genCLIDocWriteIndex(root, "authelia-gen"); err != nil { + if err = genCLIDocWriteIndex(fullPathDocsCLIReference, cmdUseRoot); err != nil { return err } @@ -69,6 +73,16 @@ func docsCLIRunE(cmd *cobra.Command, args []string) (err error) { } func genCLIDoc(cmd *cobra.Command, path string) (err error) { + if _, err = os.Stat(path); err != nil && !os.IsNotExist(err) { + return err + } + + if err == nil || !os.IsNotExist(err) { + if err = os.RemoveAll(path); err != nil { + return fmt.Errorf("failed to remove docs: %w", err) + } + } + if err = os.Mkdir(path, 0755); err != nil { if !os.IsExist(err) { return err diff --git a/cmd/authelia-gen/cmd_docs_date.go b/cmd/authelia-gen/cmd_docs_date.go index 7d9f83f85..b7a2f2794 100644 --- a/cmd/authelia-gen/cmd_docs_date.go +++ b/cmd/authelia-gen/cmd_docs_date.go @@ -17,14 +17,13 @@ import ( func newDocsDateCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "date", + Use: cmdUseDocsDate, Short: "Generate doc dates", RunE: docsDateRunE, DisableAutoGenTag: true, } - cmd.Flags().StringP("directory", "d", "./docs/content", "The directory to modify") cmd.Flags().String("commit-until", "HEAD", "The commit to check the logs until") cmd.Flags().String("commit-since", "", "The commit to check the logs since") @@ -33,14 +32,18 @@ func newDocsDateCmd() *cobra.Command { func docsDateRunE(cmd *cobra.Command, args []string) (err error) { var ( - dir, cwd, commitUtil, commitSince, commitFilter string + root, pathDocsContent, cwd, commitUtil, commitSince, commitFilter string ) - if dir, err = cmd.Flags().GetString("directory"); err != nil { + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { return err } - if cwd, err = cmd.Flags().GetString("cwd"); err != nil { + if pathDocsContent, err = cmd.Flags().GetString(cmdFlagDocsContent); err != nil { + return err + } + + if cwd, err = cmd.Flags().GetString(cmdFlagCwd); err != nil { return err } @@ -56,7 +59,7 @@ func docsDateRunE(cmd *cobra.Command, args []string) (err error) { commitFilter = fmt.Sprintf("%s...%s", commitUtil, commitSince) } - return filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { + return filepath.Walk(filepath.Join(root, pathDocsContent), func(path string, info fs.FileInfo, err error) error { if err != nil { return err } @@ -163,7 +166,7 @@ func replaceDates(path string, date time.Time, dateGit *time.Time) { for scanner.Scan() { if found < 2 && frontmatter < 2 { switch { - case scanner.Text() == frontmatterDelimiterLine: + case scanner.Text() == delimiterLineFrontMatter: buf.Write(scanner.Bytes()) frontmatter++ case frontmatter != 0 && strings.HasPrefix(scanner.Text(), "date: "): @@ -207,13 +210,13 @@ func getFrontmatter(path string) []byte { for scanner.Scan() { if start { - if scanner.Text() == frontmatterDelimiterLine { + if scanner.Text() == delimiterLineFrontMatter { break } buf.Write(scanner.Bytes()) buf.Write(newline) - } else if scanner.Text() == frontmatterDelimiterLine { + } else if scanner.Text() == delimiterLineFrontMatter { start = true } } diff --git a/cmd/authelia-gen/cmd_docs_keys.go b/cmd/authelia-gen/cmd_docs_keys.go new file mode 100644 index 000000000..df6763acb --- /dev/null +++ b/cmd/authelia-gen/cmd_docs_keys.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/spf13/cobra" + + "github.com/authelia/authelia/v4/internal/configuration" + "github.com/authelia/authelia/v4/internal/configuration/schema" +) + +func newDocsKeysCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseKeys, + Short: "Generate the docs data file for configuration keys", + RunE: docsKeysRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func docsKeysRunE(cmd *cobra.Command, args []string) (err error) { + //nolint:prealloc + var ( + pathDocsConfigKeys, root string + data []ConfigurationKey + ) + + keys := readTags("", reflect.TypeOf(schema.Configuration{})) + + for _, key := range keys { + if strings.Contains(key, "[]") { + continue + } + + ck := ConfigurationKey{ + Path: key, + Secret: configuration.IsSecretKey(key), + } + + switch { + case ck.Secret: + ck.Env = configuration.ToEnvironmentSecretKey(key, configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter) + default: + ck.Env = configuration.ToEnvironmentKey(key, configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter) + } + + data = append(data, ck) + } + + var ( + dataJSON []byte + ) + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if pathDocsConfigKeys, err = cmd.Flags().GetString(cmdFlagFileDocsKeys); err != nil { + return err + } + + fullPathDocsConfigKeys := filepath.Join(root, pathDocsConfigKeys) + + if dataJSON, err = json.Marshal(data); err != nil { + return err + } + + if err = os.WriteFile(fullPathDocsConfigKeys, dataJSON, 0600); err != nil { + return fmt.Errorf("failed to write file '%s': %w", fullPathDocsConfigKeys, err) + } + + return nil +} diff --git a/cmd/authelia-gen/cmd_github.go b/cmd/authelia-gen/cmd_github.go new file mode 100644 index 000000000..8d8ec2f49 --- /dev/null +++ b/cmd/authelia-gen/cmd_github.go @@ -0,0 +1,236 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/spf13/cobra" +) + +func newGitHubCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseGitHub, + Short: "Generate GitHub files", + RunE: rootSubCommandsRunE, + + DisableAutoGenTag: true, + } + + cmd.AddCommand(newGitHubIssueTemplatesCmd()) + + return cmd +} + +func newGitHubIssueTemplatesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseGitHubIssueTemplates, + Short: "Generate GitHub issue templates", + RunE: rootSubCommandsRunE, + + DisableAutoGenTag: true, + } + + cmd.AddCommand(newGitHubIssueTemplatesBugReportCmd(), newGitHubIssueTemplatesFeatureCmd()) + + return cmd +} + +func newGitHubIssueTemplatesFeatureCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseGitHubIssueTemplatesFR, + Short: "Generate GitHub feature request issue template", + RunE: cmdGitHubIssueTemplatesFeatureRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func newGitHubIssueTemplatesBugReportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseGitHubIssueTemplatesBR, + Short: "Generate GitHub bug report issue template", + RunE: cmdGitHubIssueTemplatesBugReportRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func cmdGitHubIssueTemplatesFeatureRunE(cmd *cobra.Command, args []string) (err error) { + var ( + cwd, file, root string + tags, tagsFuture []string + latestMajor, latestMinor, latestPatch, versions int + ) + + if cwd, err = cmd.Flags().GetString(cmdFlagCwd); err != nil { + return err + } + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if file, err = cmd.Flags().GetString(cmdFlagFeatureRequest); err != nil { + return err + } + + if versions, err = cmd.Flags().GetInt(cmdFlagVersions); err != nil { + return err + } + + if tags, err = getGitTags(cwd); err != nil { + return err + } + + latest := tags[0] + + if _, err = fmt.Sscanf(latest, "v%d.%d.%d", &latestMajor, &latestMinor, &latestPatch); err != nil { + return fmt.Errorf("error occurred parsing version as semver: %w", err) + } + + var ( + minor int + ) + + for minor = latestMinor + 1; minor < latestMinor+versions; minor++ { + tagsFuture = append(tagsFuture, fmt.Sprintf("v%d.%d.0", latestMajor, minor)) + } + + tagsFuture = append(tagsFuture, fmt.Sprintf("v%d.0.0", latestMajor+1)) + + var ( + f *os.File + ) + + fullPath := filepath.Join(root, file) + + if f, err = os.Create(fullPath); err != nil { + return fmt.Errorf("failed to create file '%s': %w", fullPath, err) + } + + data := &tmplIssueTemplateData{ + Labels: []string{labelTypeFeature.String(), labelStatusNeedsDesign.String(), labelPriorityNormal.String()}, + Versions: tagsFuture, + } + + if err = tmplIssueTemplateFeature.Execute(f, data); err != nil { + return err + } + + return nil +} + +func cmdGitHubIssueTemplatesBugReportRunE(cmd *cobra.Command, args []string) (err error) { + var ( + cwd, file, dirRoot string + latestMinor, versions int + + tags []string + ) + + if cwd, err = cmd.Flags().GetString(cmdFlagCwd); err != nil { + return err + } + + if dirRoot, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if file, err = cmd.Flags().GetString(cmdFlagBugReport); err != nil { + return err + } + + if versions, err = cmd.Flags().GetInt(cmdFlagVersions); err != nil { + return err + } + + if tags, err = getGitTags(cwd); err != nil { + return err + } + + latest := tags[0] + + latestParts := strings.Split(latest, ".") + + if len(latestParts) < 2 { + return fmt.Errorf("error extracting latest minor version from tag: %s does not appear to be a semver", latest) + } + + if latestMinor, err = strconv.Atoi(latestParts[1]); err != nil { + return fmt.Errorf("error extracting latest minor version from tag: %w", err) + } + + //nolint:prealloc + var ( + tagsRecent []string + parts []string + minor int + ) + + for _, tag := range tags { + if parts = strings.Split(tag, "."); len(parts) < 2 { + return fmt.Errorf("error extracting minor version from tag: %s does not appear to be a semver", tag) + } + + if minor, err = strconv.Atoi(parts[1]); err != nil { + return fmt.Errorf("error extracting minor version from tag: %w", err) + } + + if minor < latestMinor-versions { + break + } + + tagsRecent = append(tagsRecent, tag) + } + + var ( + f *os.File + ) + + fullPath := filepath.Join(dirRoot, file) + + if f, err = os.Create(fullPath); err != nil { + return fmt.Errorf("failed to create file '%s': %w", fullPath, err) + } + + data := &tmplIssueTemplateData{ + Labels: []string{labelTypeBugUnconfirmed.String(), labelStatusNeedsTriage.String(), labelPriorityNormal.String()}, + Versions: tagsRecent, + Proxies: []string{"Caddy", "Traefik", "Envoy", "NGINX", "SWAG", "NGINX Proxy Manager", "HAProxy"}, + } + + if err = tmplGitHubIssueTemplateBug.Execute(f, data); err != nil { + return err + } + + return nil +} + +func getGitTags(cwd string) (tags []string, err error) { + var ( + args []string + tagsOutput []byte + ) + + if len(cwd) != 0 { + args = append(args, "-C", cwd) + } + + args = append(args, "tag", "--sort=-creatordate") + + cmd := exec.Command("git", args...) + + if tagsOutput, err = cmd.Output(); err != nil { + return nil, err + } + + return strings.Split(string(tagsOutput), "\n"), nil +} diff --git a/cmd/authelia-gen/cmd_locales.go b/cmd/authelia-gen/cmd_locales.go new file mode 100644 index 000000000..6948ac7b4 --- /dev/null +++ b/cmd/authelia-gen/cmd_locales.go @@ -0,0 +1,215 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/spf13/cobra" + "golang.org/x/text/language" + "golang.org/x/text/language/display" + + "github.com/authelia/authelia/v4/internal/utils" +) + +func newLocalesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseLocales, + Short: "Generate locales files", + RunE: localesRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func localesRunE(cmd *cobra.Command, args []string) (err error) { + var ( + root, pathLocales string + pathWebI18NIndex, pathDocsDataLanguages string + ) + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if pathLocales, err = cmd.Flags().GetString(cmdFlagDirLocales); err != nil { + return err + } + + if pathWebI18NIndex, err = cmd.Flags().GetString(cmdFlagFileWebI18N); err != nil { + return err + } + + if pathDocsDataLanguages, err = cmd.Flags().GetString(cmdFlagDocsDataLanguages); err != nil { + return err + } + + data, err := getLanguages(filepath.Join(root, pathLocales)) + if err != nil { + return err + } + + fullPathWebI18NIndex := filepath.Join(root, pathWebI18NIndex) + + var ( + f *os.File + dataJSON []byte + ) + + if f, err = os.Create(fullPathWebI18NIndex); err != nil { + return fmt.Errorf("failed to create file '%s': %w", fullPathWebI18NIndex, err) + } + + if err = tmplWebI18NIndex.Execute(f, data); err != nil { + return err + } + + if dataJSON, err = json.Marshal(data); err != nil { + return err + } + + fullPathDocsDataLanguages := filepath.Join(root, pathDocsDataLanguages) + + if err = os.WriteFile(fullPathDocsDataLanguages, dataJSON, 0600); err != nil { + return fmt.Errorf("failed to write file '%s': %w", fullPathDocsDataLanguages, err) + } + + return nil +} + +//nolint:gocyclo +func getLanguages(dir string) (languages *Languages, err error) { + //nolint:prealloc + var locales []string + + languages = &Languages{ + Defaults: DefaultsLanguages{ + Namespace: localeNamespaceDefault, + }, + } + + var defaultTag language.Tag + + if defaultTag, err = language.Parse(localeDefault); err != nil { + return nil, fmt.Errorf("failed to parse default language: %w", err) + } + + languages.Defaults.Language = Language{ + Display: display.English.Tags().Name(defaultTag), + Locale: localeDefault, + } + + if err = filepath.Walk(dir, func(path string, info fs.FileInfo, errWalk error) (err error) { + if errWalk != nil { + return errWalk + } + + nameLower := strings.ToLower(info.Name()) + ext := filepath.Ext(nameLower) + ns := strings.Replace(nameLower, ext, "", 1) + + if ext != ".json" { + return nil + } + + if !utils.IsStringInSlice(ns, languages.Namespaces) { + languages.Namespaces = append(languages.Namespaces, ns) + } + + fdir, _ := filepath.Split(path) + + locale := filepath.Base(fdir) + + if utils.IsStringInSlice(locale, locales) { + for i, l := range languages.Languages { + if l.Locale == locale { + if utils.IsStringInSlice(ns, languages.Languages[i].Namespaces) { + break + } + + languages.Languages[i].Namespaces = append(languages.Languages[i].Namespaces, ns) + break + } + } + + return nil + } + + var localeReal string + + parts := strings.SplitN(locale, "-", 2) + if len(parts) == 2 && strings.EqualFold(parts[0], parts[1]) { + localeReal = parts[0] + } else { + localeReal = locale + } + + var tag language.Tag + + if tag, err = language.Parse(localeReal); err != nil { + return fmt.Errorf("failed to parse language '%s': %w", localeReal, err) + } + + l := Language{ + Display: display.English.Tags().Name(tag), + Locale: localeReal, + Namespaces: []string{ns}, + Fallbacks: []string{languages.Defaults.Language.Locale}, + Tag: tag, + } + + languages.Languages = append(languages.Languages, l) + + locales = append(locales, l.Locale) + + return nil + }); err != nil { + return nil, err + } + + var langs []Language //nolint:prealloc + + for i, lang := range languages.Languages { + p := lang.Tag.Parent() + + if p.String() == "und" || strings.Contains(p.String(), "-") { + continue + } + + if utils.IsStringInSlice(p.String(), locales) { + continue + } + + if p.String() != lang.Locale { + lang.Fallbacks = append([]string{p.String()}, lang.Fallbacks...) + } + + languages.Languages[i] = lang + + l := Language{ + Display: display.English.Tags().Name(p), + Locale: p.String(), + Namespaces: lang.Namespaces, + Fallbacks: []string{languages.Defaults.Language.Locale}, + Tag: p, + } + + langs = append(langs, l) + + locales = append(locales, l.Locale) + } + + languages.Languages = append(languages.Languages, langs...) + + sort.Slice(languages.Languages, func(i, j int) bool { + return languages.Languages[i].Locale == localeDefault || languages.Languages[i].Locale < languages.Languages[j].Locale + }) + + return languages, nil +} diff --git a/cmd/authelia-gen/cmd_root.go b/cmd/authelia-gen/cmd_root.go new file mode 100644 index 000000000..1ce6cf2dc --- /dev/null +++ b/cmd/authelia-gen/cmd_root.go @@ -0,0 +1,124 @@ +package main + +import ( + "sort" + "strings" + + "github.com/spf13/cobra" + + "github.com/authelia/authelia/v4/internal/utils" +) + +var rootCmd *cobra.Command + +func init() { + rootCmd = newRootCmd() +} + +func newRootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseRoot, + Short: "Authelia's generator tooling", + RunE: rootSubCommandsRunE, + + DisableAutoGenTag: true, + } + + cmd.PersistentFlags().StringP(cmdFlagCwd, "C", "", "Sets the CWD for git commands") + cmd.PersistentFlags().StringP(cmdFlagRoot, "d", dirCurrent, "The repository root") + cmd.PersistentFlags().StringSliceP(cmdFlagExclude, "X", nil, "Sets the names of excluded generators") + cmd.PersistentFlags().String(cmdFlagFeatureRequest, fileGitHubIssueTemplateFR, "Sets the path of the feature request issue template file") + cmd.PersistentFlags().String(cmdFlagBugReport, fileGitHubIssueTemplateBR, "Sets the path of the bug report issue template file") + cmd.PersistentFlags().Int(cmdFlagVersions, 5, "the maximum number of minor versions to list in output templates") + cmd.PersistentFlags().String(cmdFlagDirLocales, dirLocales, "The locales directory in relation to the root") + cmd.PersistentFlags().String(cmdFlagFileWebI18N, fileWebI18NIndex, "The i18n typescript configuration file in relation to the root") + cmd.PersistentFlags().String(cmdFlagDocsDataLanguages, fileDocsDataLanguages, "The languages docs data file in relation to the docs data folder") + cmd.PersistentFlags().String(cmdFlagDocsCLIReference, dirDocsCLIReference, "The directory to store the markdown in") + cmd.PersistentFlags().String(cmdFlagDocsContent, dirDocsContent, "The directory with the docs content") + cmd.PersistentFlags().String(cmdFlagFileConfigKeys, fileCodeConfigKeys, "Sets the path of the keys file") + cmd.PersistentFlags().String(cmdFlagFileDocsKeys, fileDocsConfigKeys, "Sets the path of the docs keys file") + cmd.PersistentFlags().String(cmdFlagPackageConfigKeys, pkgConfigSchema, "Sets the package name of the keys file") + cmd.PersistentFlags().String(cmdFlagFileScriptsGen, fileScriptsGen, "Sets the path of the authelia-scripts gen file") + cmd.PersistentFlags().String(cmdFlagPackageScriptsGen, pkgScriptsGen, "Sets the package name of the authelia-scripts gen file") + cmd.PersistentFlags().String(cmdFlagFileConfigCommitLint, fileCICommitLintConfig, "The commit lint javascript configuration file in relation to the root") + cmd.PersistentFlags().String(cmdFlagFileDocsCommitMsgGuidelines, fileDocsCommitMessageGuidelines, "The commit message guidelines documentation file in relation to the root") + + cmd.AddCommand(newCodeCmd(), newDocsCmd(), newGitHubCmd(), newLocalesCmd(), newCommitLintCmd()) + + return cmd +} + +func rootSubCommandsRunE(cmd *cobra.Command, args []string) (err error) { + var exclude []string + + if exclude, err = cmd.Flags().GetStringSlice(cmdFlagExclude); err != nil { + return err + } + + subCmds := cmd.Commands() + + switch cmd.Use { + case cmdUseRoot: + sort.Slice(subCmds, func(i, j int) bool { + switch subCmds[j].Use { + case cmdUseDocs: + // Ensure `docs` subCmd is last. + return true + default: + return subCmds[i].Use < subCmds[j].Use + } + }) + case cmdUseDocs: + sort.Slice(subCmds, func(i, j int) bool { + switch subCmds[j].Use { + case cmdUseDocsDate: + // Ensure `date` subCmd is last. + return true + default: + return subCmds[i].Use < subCmds[j].Use + } + }) + default: + sort.Slice(subCmds, func(i, j int) bool { + return subCmds[i].Use < subCmds[j].Use + }) + } + + for _, subCmd := range subCmds { + if subCmd.Use == cmdUseCompletion || strings.HasPrefix(subCmd.Use, "help ") || utils.IsStringSliceContainsAny([]string{resolveCmdName(subCmd), subCmd.Use}, exclude) { + continue + } + + rootCmd.SetArgs(rootCmdGetArgs(subCmd, args)) + + if err = rootCmd.Execute(); err != nil { + return err + } + } + + return nil +} + +func resolveCmdName(cmd *cobra.Command) string { + parent := cmd.Parent() + + if parent != nil && parent.Use != cmd.Use && parent.Use != cmdUseRoot { + return resolveCmdName(parent) + "." + cmd.Use + } + + return cmd.Use +} + +func rootCmdGetArgs(cmd *cobra.Command, args []string) []string { + for { + if cmd == rootCmd { + break + } + + args = append([]string{cmd.Use}, args...) + + cmd = cmd.Parent() + } + + return args +} diff --git a/cmd/authelia-gen/const.go b/cmd/authelia-gen/const.go index 71f3ada18..e7b8eb980 100644 --- a/cmd/authelia-gen/const.go +++ b/cmd/authelia-gen/const.go @@ -1,7 +1,81 @@ package main const ( - dateFmtRFC2822 = "Mon, _2 Jan 2006 15:04:05 -0700" - dateFmtYAML = "2006-01-02T15:04:05-07:00" - frontmatterDelimiterLine = "---" + dirCurrent = "./" + dirLocales = "internal/server/locales" + + subPathCmd = "cmd" + subPathInternal = "internal" + + fileCICommitLintConfig = "web/.commitlintrc.js" + fileWebI18NIndex = "web/src/i18n/index.ts" + + fileDocsCommitMessageGuidelines = "docs/content/en/contributing/development/guidelines-commit-message.md" + + fileDocsConfigKeys = "docs/data/configkeys.json" + fileCodeConfigKeys = "internal/configuration/schema/keys.go" + fileScriptsGen = "cmd/authelia-scripts/cmd/gen.go" + + dirDocsContent = "docs/content" + dirDocsCLIReference = dirDocsContent + "/en/reference/cli" + + fileDocsDataLanguages = "docs/data/languages.json" + + fileGitHubIssueTemplateFR = ".github/ISSUE_TEMPLATE/feature-request.yml" + fileGitHubIssueTemplateBR = ".github/ISSUE_TEMPLATE/bug-report.yml" +) + +const ( + dateFmtRFC2822 = "Mon, _2 Jan 2006 15:04:05 -0700" + dateFmtYAML = "2006-01-02T15:04:05-07:00" +) + +const ( + delimiterLineFrontMatter = "---" + + localeDefault = "en" + localeNamespaceDefault = "portal" +) + +const ( + pkgConfigSchema = "schema" + pkgScriptsGen = "cmd" +) + +const ( + cmdUseRoot = "authelia-gen" + cmdUseCompletion = "completion" + cmdUseDocs = "docs" + cmdUseDocsDate = "date" + cmdUseDocsCLI = "cli" + cmdUseGitHub = "github" + cmdUseGitHubIssueTemplates = "issue-templates" + cmdUseGitHubIssueTemplatesFR = "feature-request" + cmdUseGitHubIssueTemplatesBR = "bug-report" + cmdUseLocales = "locales" + cmdUseCommitLint = "commit-lint" + cmdUseCode = "code" + cmdUseCodeScripts = "scripts" + cmdUseKeys = "keys" +) + +const ( + cmdFlagRoot = "dir.root" + cmdFlagExclude = "exclude" + cmdFlagVersions = "versions" + cmdFlagDirLocales = "dir.locales" + cmdFlagDocsCLIReference = "dir.docs.cli-reference" + cmdFlagDocsContent = "dir.docs.content" + cmdFlagDocsDataLanguages = "file.docs.data.languages" + cmdFlagCwd = "cwd" + cmdFlagFileConfigKeys = "file.configuration-keys" + cmdFlagFileDocsKeys = "file.docs-keys" + cmdFlagFileScriptsGen = "file.scripts.gen" + cmdFlagFileConfigCommitLint = "file.commit-lint-config" + cmdFlagFileDocsCommitMsgGuidelines = "file.docs-commit-msg-guidelines" + cmdFlagFileWebI18N = "file.web-i18n" + cmdFlagFeatureRequest = "file.feature-request" + cmdFlagBugReport = "file.bug-report" + cmdFlagPackageConfigKeys = "package.configuration.keys" + cmdFlagPackageScriptsGen = "package.scripts.gen" ) diff --git a/cmd/authelia-gen/main.go b/cmd/authelia-gen/main.go index 15a8d4586..ffbdca170 100644 --- a/cmd/authelia-gen/main.go +++ b/cmd/authelia-gen/main.go @@ -1,29 +1,7 @@ package main -import ( - "embed" - - "github.com/spf13/cobra" -) - -//go:embed templates/* -var templatesFS embed.FS - func main() { - if err := newRootCmd().Execute(); err != nil { + if err := rootCmd.Execute(); err != nil { panic(err) } } - -func newRootCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "authelia-gen", - Short: "Authelia's generator tooling", - - DisableAutoGenTag: true, - } - - cmd.AddCommand(newAllCmd(), newCodeCmd(), newDocsCmd()) - - return cmd -} diff --git a/cmd/authelia-gen/templates.go b/cmd/authelia-gen/templates.go new file mode 100644 index 000000000..4f65a4b18 --- /dev/null +++ b/cmd/authelia-gen/templates.go @@ -0,0 +1,69 @@ +package main + +import ( + "embed" + "fmt" + "strings" + "text/template" +) + +//go:embed templates/* +var templatesFS embed.FS + +var ( + funcMap = template.FuncMap{ + "stringsContains": strings.Contains, + "join": strings.Join, + "joinX": fmJoinX, + } + + tmplCodeConfigurationSchemaKeys = template.Must(newTMPL("internal_configuration_schema_keys.go")) + tmplGitHubIssueTemplateBug = template.Must(newTMPL("github_issue_template_bug_report.yml")) + tmplIssueTemplateFeature = template.Must(newTMPL("github_issue_template_feature.yml")) + tmplWebI18NIndex = template.Must(newTMPL("web_i18n_index.ts")) + tmplDotCommitLintRC = template.Must(newTMPL("dot_commitlintrc.js")) + tmplDocsCommitMessageGuidelines = template.Must(newTMPL("docs-contributing-development-commitmsg.md")) + tmplScriptsGen = template.Must(newTMPL("cmd-authelia-scripts-gen.go")) +) + +func fmJoinX(elems []string, sep string, n int, p string) string { + buf := strings.Builder{} + + c := 0 + e := len(elems) - 1 + + for i := 0; i <= e; i++ { + if c+len(elems[i])+1 > n { + c = 0 + + buf.WriteString(p) + } + + c += len(elems[i]) + 1 + + buf.WriteString(elems[i]) + + if i < e { + buf.WriteString(sep) + } + } + + return buf.String() +} + +func newTMPL(name string) (tmpl *template.Template, err error) { + return template.New(name).Funcs(funcMap).Parse(mustLoadTmplFS(name)) +} + +func mustLoadTmplFS(tmpl string) string { + var ( + content []byte + err error + ) + + if content, err = templatesFS.ReadFile(fmt.Sprintf("templates/%s.tmpl", tmpl)); err != nil { + panic(err) + } + + return string(content) +} diff --git a/cmd/authelia-gen/templates/cmd-authelia-scripts-gen.go.tmpl b/cmd/authelia-gen/templates/cmd-authelia-scripts-gen.go.tmpl new file mode 100644 index 000000000..5955d5f7d --- /dev/null +++ b/cmd/authelia-gen/templates/cmd-authelia-scripts-gen.go.tmpl @@ -0,0 +1,11 @@ +// Code generated by go generate. DO NOT EDIT. +// +// Run the following command to generate this file: +// go run ./cmd/authelia-gen code scripts +// + +package {{ .Package }} + +const ( + versionSwaggerUI = "{{ .VersionSwaggerUI }}" +) diff --git a/cmd/authelia-gen/templates/docs-contributing-development-commitmsg.md.tmpl b/cmd/authelia-gen/templates/docs-contributing-development-commitmsg.md.tmpl new file mode 100644 index 000000000..93b960316 --- /dev/null +++ b/cmd/authelia-gen/templates/docs-contributing-development-commitmsg.md.tmpl @@ -0,0 +1,138 @@ +--- +title: "Commit Message Guidelines" +description: "Authelia Development Commit Message Guidelines" +lead: "This section covers the git commit message guidelines we use for development." +date: 2021-01-30T19:29:07+11:00 +draft: false +images: [] +menu: + contributing: + parent: "development" +weight: 231 +toc: true +aliases: + - /docs/contributing/commitmsg-guidelines.html +--- + +The reasons for these conventions are as follows: + +* simple navigation though git history +* easier to read git history + +## Commit Message Format + +Each commit message consists of a __header__, a __body__, and a __footer__. + +```bash +
+ + + +