From 83e83f19ca7f44adca9c680106e6563df3923ff2 Mon Sep 17 00:00:00 2001 From: Yuna Morgenstern Date: Wed, 10 Jun 2026 19:31:19 +0200 Subject: [PATCH 1/8] Harden Gitea workflow compatibility --- CHANGELOG.md | 1 + README.md | 13 +- ...02-configurable-remote-action-providers.md | 7 + doc/navigation.md | 1 + doc/spec/editor-test-matrix.md | 2 + .../gitea-github-actions-compatibility.md | 54 +++++ .../git/RemoteActionProviders.java | 202 ++++++++++++++++- .../githubworkflow/run/WorkflowRun.java | 44 +++- .../run/WorkflowRunProcessHandler.java | 6 +- .../git/GiteaDockerIntegrationTest.java | 212 ++++++++++++++++++ .../git/RemoteActionProvidersTest.java | 101 +++++++++ .../run/WorkflowRunProcessHandlerTest.java | 45 ++++ .../githubworkflow/run/WorkflowRunTest.java | 30 +++ 13 files changed, 696 insertions(+), 22 deletions(-) create mode 100644 doc/spec/gitea-github-actions-compatibility.md create mode 100644 src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab3979..1a0b4b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Updated the IntelliJ test platform, JaCoCo, and GitHub Actions cache action to current stable metadata. - Fixed the README build badge link and refreshed navigation/release docs. - `.gitea/workflows/*` files now get their own light/dark Gitea-flavored file icon instead of cosplaying GitHub. +- Gitea workflow runs now use `GITEA_TOKEN`-style auth, clean browser links, and repo-level run discovery for `/api/v1`. ## [2026.5.29] - 2026-05-29 diff --git a/README.md b/README.md index fccd47b..1db905f 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,20 @@ The project uses the Gradle wrapper and Java 25. No manual JetBrains JDK path is Plugin downloads the IDE, bundled plugins, verifier, and test runtime. 1. Install Java 25 and make it available as `java`. -2. Run `./gradlew test` for the fast regression suite. +2. Run `./gradlew test` for the regression suite, including the Docker-backed Gitea smoke test. 3. Run `./gradlew check verifyPlugin buildPlugin` before publishing or opening a release PR. +Gitea smoke test controls: + +```sh +./gradlew test --tests com.github.yunabraska.githubworkflow.git.GiteaDockerIntegrationTest --rerun-tasks +GITEA_DOCKER_TEST=false ./gradlew test +``` + +That starts the official rootless Gitea Docker image, seeds a tiny repository, and checks action plus `.gitea/workflows` +metadata through the same remote resolver. Set `GITEA_DOCKER_TEST=false` to skip it locally, or override the image with +`GITEA_IMAGE` when testing another Gitea release. + ## Release Automation One workflow handles tagging, packaging, GitHub Packages, Marketplace publishing, changelog notes, and GitHub releases. diff --git a/doc/adr/0002-configurable-remote-action-providers.md b/doc/adr/0002-configurable-remote-action-providers.md index 6c01879..2d74222 100644 --- a/doc/adr/0002-configurable-remote-action-providers.md +++ b/doc/adr/0002-configurable-remote-action-providers.md @@ -24,11 +24,18 @@ tokens. Use a JDK fake HTTP server in tests for GitHub Enterprise-shaped API behavior. Tests may inject temporary server definitions directly into the service; that is not a user-facing settings surface. +Reuse the same provider boundary for Gitea-compatible API behavior where possible. Gitea differs by using `/api/v1` and +`Authorization: token ...`, so tests cover that provider type explicitly. Do not add a Gitea account UI unless real +user-facing configuration becomes necessary. + ## Consequences Self-hosted GitHub action metadata can be resolved, linked, highlighted, styled, documented, and completed without contacting public GitHub in tests. +Gitea action and `.gitea/workflows` metadata can be tested through fake `/api/v1` responses and a default-on Docker +smoke test without duplicating the GitHub resolver. `GITEA_DOCKER_TEST=false` keeps local escape hatches explicit. + The plugin stays boring: no duplicate account UI, no token storage, and fewer settings to test. The provider currently resolves metadata and refs after a callable is known. It does not browse arbitrary remote diff --git a/doc/navigation.md b/doc/navigation.md index ad13167..dd33099 100644 --- a/doc/navigation.md +++ b/doc/navigation.md @@ -38,6 +38,7 @@ Editor behavior should be tested through IntelliJ fixture entrypoints. See: - `doc/adr/0008-test-through-editor-and-runtime-boundaries.md` - `doc/spec/editor-test-matrix.md` +- `doc/spec/gitea-github-actions-compatibility.md` Remote GitHub behavior should use fake HTTP servers or explicit client boundaries. Network access in tests is guilty until proven innocent. diff --git a/doc/spec/editor-test-matrix.md b/doc/spec/editor-test-matrix.md index 35af12c..51798b4 100644 --- a/doc/spec/editor-test-matrix.md +++ b/doc/spec/editor-test-matrix.md @@ -21,6 +21,8 @@ Official syntax references: - Resolved actions with cached major-version refs show an update quick-fix for older `v?\d+` refs. - Remote `uses` ref completion for tags/branches resolved from public GitHub and GitHub Enterprise-shaped servers through fake HTTP servers. +- Gitea-compatible `/api/v1` remote metadata resolution is covered through fake HTTP tests and a default-on Docker smoke + test against the official rootless Gitea image on Unix-like hosts. Set `GITEA_DOCKER_TEST=false` to skip it. - GitHub Enterprise servers registered in JetBrains GitHub settings are used as remote metadata sources; the plugin does not add a parallel server settings UI. - Cache actions and settings are registered through `plugin.xml`, localized through resource bundles, and covered by diff --git a/doc/spec/gitea-github-actions-compatibility.md b/doc/spec/gitea-github-actions-compatibility.md new file mode 100644 index 0000000..9a3c2d7 --- /dev/null +++ b/doc/spec/gitea-github-actions-compatibility.md @@ -0,0 +1,54 @@ +# Gitea And GitHub Actions Compatibility + +Last checked: 2026-06-10. + +This note tracks behavior where Gitea Actions is close to GitHub Actions, but not identical. The plugin should prefer +shared behavior first and branch only where Gitea really differs. Tiny forks, not a hydra. + +## Sources + +- Gitea Actions comparison: https://docs.gitea.com/usage/actions/comparison +- Gitea Actions job token permissions: https://docs.gitea.com/usage/actions/token-permissions +- Gitea Actions variables and `gitea.*` context: https://docs.gitea.com/usage/actions/actions-variables +- Gitea API reference and OpenAPI spec: https://docs.gitea.com/api/ +- Gitea OpenAPI source used for endpoint checks: https://docs.gitea.com/redocusaurus/plugin-redoc-1.yaml +- GitHub workflow REST API: https://docs.github.com/en/rest/actions/workflows?apiVersion=2026-03-10 +- GitHub workflow run REST API: https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2026-03-10 +- GitHub workflow job REST API: https://docs.github.com/en/rest/actions/workflow-jobs?apiVersion=2026-03-10 +- GitHub artifact REST API: https://docs.github.com/en/rest/actions/artifacts?apiVersion=2026-03-10 + +## Already Handled + +| Area | GitHub | Gitea | Plugin behavior | +| --- | --- | --- | --- | +| Workflow home | `.github/workflows/*` | `.gitea/workflows/*` | Both workflow roots are detected. | +| API base | `https://api.github.com` or enterprise `/api/v3` | Instance `/api/v1` | Run and metadata code infer provider behavior from API URL and workflow path. | +| API auth header | `Authorization: Bearer ...` | `Authorization: token ...` | Gitea providers and runs use `token ...`; GitHub keeps IDE account plus bearer token priority. | +| Token env fallback | `GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_PAT` | `GITEA_TOKEN`, `GITEA_PAT` | Gitea runs and metadata use Gitea token env names before anonymous access. | +| Workflow dispatch result | GitHub returns run details on current API version | Gitea returns run details when `return_run_details=true` | Gitea dispatch adds `return_run_details=true`; both parse `workflow_run_id`, `run_url`, and `html_url`. | +| Run discovery | Workflow-scoped or repository-scoped run listing | Repository-scoped `actions/runs` with `limit` | GitHub keeps workflow-scoped discovery; Gitea uses repo-level discovery with `limit=1`. | +| Job logs | `/actions/jobs/{job_id}/logs` | Same path in Gitea OpenAPI | Existing job log download path is shared. | +| Run jobs | `/actions/runs/{run}/jobs` | Same path in Gitea OpenAPI | Existing job tree polling path is shared. | +| Artifacts | Run artifact list plus artifact ZIP | Same core paths in Gitea OpenAPI | Existing artifact list/download path is shared. | +| Context root | `github.*` | `gitea.*`, with `github.*` as alias | Completion/highlighting covers `gitea.*` using the GitHub-compatible key map. | +| Absolute action URLs | GitHub syntax is usually `owner/repo[/path]@ref` | Absolute URLs are supported | Remote resolver supports absolute URLs for configured servers. | + +## Differences To Keep Watching + +| Area | Gitea behavior | Plugin risk | Suggested handling | +| --- | --- | --- | --- | +| Non-standard cron aliases | Gitea supports `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`; GitHub does not. | GitHub-style validation may mark valid Gitea schedules as odd. | Add Gitea-aware completion/validation once workflow provider detection is available in syntax checks. | +| Ignored job keys | Gitea currently ignores `jobs..timeout-minutes`, `continue-on-error`, and `environment`. | Warnings should not imply Gitea will enforce these keys. | Keep syntax valid, optionally show Gitea-specific weak info later. | +| Complex `runs-on` | Gitea supports scalar or single-item array forms, not GitHub's richer runner targeting. | Completion may suggest shapes Gitea does not execute. | Keep GitHub completion by default; add Gitea-aware validation only for `.gitea/workflows`. | +| Expressions | Gitea comparison says only `always()` is supported from expression functions. | GitHub-rich expression completion can overpromise in Gitea files. | Treat as future Gitea-specific inspection, not a parser fork. | +| Permission scopes | Gitea supports a subset plus `code`, `releases`, `wiki`, `projects`; GitHub supports scopes such as `statuses`, `checks`, `deployments`, `id-token`, `security-events`, and `pages`. | Current permission completion is GitHub-shaped. | Add provider-aware permission scopes for `.gitea/workflows`. | +| Problem matchers and annotations | Gitea ignores problem matchers and workflow command annotations. | Log rendering can still color warnings/errors locally; remote UI may not. | No code change needed unless we add Gitea-specific docs. | +| Default action source | Gitea may resolve unqualified `uses:` through instance config (`github` or `self`). | Plugin cannot know server admin config. | Keep configured-server absolute URL support; do not guess admin config. | +| Secret and variable names | Gitea disallows user-created names starting with `GITHUB_` or `GITEA_`; variables are uppercased. | Future settings/UI for external variables must enforce Gitea naming rules. | Documented only; no external variable CRUD exists. | + +## Test Shape + +- Fake HTTP tests cover provider inference, auth header scheme, dispatch URL, and run discovery URL. +- The Docker-backed Gitea integration test runs by default and seeds a tiny repository to verify `/api/v1` metadata + resolution for actions and `.gitea/workflows`. +- Keep Docker test opt-out explicit with `GITEA_DOCKER_TEST=false` for machines where Docker is unavailable. diff --git a/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java b/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java index c5fb553..ad0dfba 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java @@ -136,7 +136,7 @@ private static List metadataPaths(final Server server, final RemoteUses private static boolean isWorkflowPath(final String path) { final String normalized = path.replace('\\', '/'); - return normalized.contains(".github/workflows/") + return (normalized.contains(".github/workflows/") || normalized.contains(".gitea/workflows/")) && (normalized.endsWith(".yml") || normalized.endsWith(".yaml")); } @@ -187,7 +187,7 @@ private static Map searchUses(final Server server, final RemoteU } private static Optional getJson(final Server server, final String url) { - for (final RemoteActionProviders.Authorizations.Authorization authorization : RemoteActionProviders.Authorizations.forApiUrl(server.apiUrl, server.tokenEnvVar, null)) { + for (final RemoteActionProviders.Authorizations.Authorization authorization : RemoteActionProviders.Authorizations.forServer(server, null)) { try { final HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url)) .timeout(Duration.ofSeconds(3)) @@ -374,6 +374,7 @@ static Optional parse(final Server server, final String value) public static class Settings { public static final String TYPE_GITHUB = "github"; + public static final String TYPE_GITEA = "gitea"; private final CopyOnWriteArrayList testServers = new CopyOnWriteArrayList<>(); @@ -447,7 +448,18 @@ public Server( final String tokenEnvVar, final boolean enabled ) { - this.type = Settings.TYPE_GITHUB; + this(Settings.TYPE_GITHUB, name, webUrl, apiUrl, tokenEnvVar, enabled); + } + + public Server( + final String type, + final String name, + final String webUrl, + final String apiUrl, + final String tokenEnvVar, + final boolean enabled + ) { + this.type = Optional.ofNullable(type).map(String::trim).filter(RemoteActionProviders::hasText).orElse(Settings.TYPE_GITHUB); this.name = name; this.webUrl = webUrl; this.apiUrl = apiUrl; @@ -455,6 +467,49 @@ public Server( this.enabled = enabled; } + /** + * Creates a configured Gitea remote provider. Gitea uses GitHub-compatible repository endpoints under + * {@code /api/v1}, but personal access tokens use {@code Authorization: token ...}. + * + * @param name readable server name shown in diagnostics + * @param webUrl browser URL of the Gitea instance + * @param apiUrl REST API URL, usually {@code /api/v1} + * @param tokenEnvVar optional environment variable containing a Gitea token + * @param enabled whether the provider should be used + * @return a Gitea server configuration + */ + public static Server gitea( + final String name, + final String webUrl, + final String apiUrl, + final String tokenEnvVar, + final boolean enabled + ) { + return new Server(Settings.TYPE_GITEA, name, webUrl, apiUrl, tokenEnvVar, enabled); + } + + /** + * Infers the remote provider for a workflow-run request. Gitea is detected from its workflow home, + * {@code /api/v1} API base, or a Gitea-named token variable. + * + * @param apiUrl REST API URL configured for the run + * @param workflowPath workflow file path being dispatched + * @param tokenEnvVar optional token environment variable configured for the run + * @return normalized remote provider configuration + */ + public static Server fromWorkflowRun( + final String apiUrl, + final String workflowPath, + final String tokenEnvVar + ) { + final String normalizedApiUrl = trimTrailingSlash(apiUrl); + final String webUrl = webUrlFromApiUrl(normalizedApiUrl); + if (looksLikeGitea(normalizedApiUrl, workflowPath, tokenEnvVar)) { + return gitea("Gitea", webUrl, normalizedApiUrl, tokenEnvVar, true).normalized(); + } + return new Server("GitHub", webUrl, normalizedApiUrl, tokenEnvVar, true).normalized(); + } + public boolean isEnabled() { return enabled; } @@ -463,17 +518,27 @@ public boolean isValid() { return isEnabled() && hasText(webUrl) && hasText(apiUrl); } + /** + * Reports whether this provider uses Gitea-compatible request behavior. + * + * @return {@code true} for Gitea providers + */ + public boolean isGitea() { + return Settings.TYPE_GITEA.equalsIgnoreCase(type); + } + public String authorizationHeader() { return Optional.ofNullable(tokenEnvVar) .filter(RemoteActionProviders::hasText) .map(System::getenv) .filter(RemoteActionProviders::hasText) - .map(token -> "Bearer " + token) + .map(token -> authorizationPrefix() + token) .orElse(""); } public Server normalized() { return new Server( + hasText(type) ? type.trim().toLowerCase() : Settings.TYPE_GITHUB, hasText(name) ? name.trim() : webUrl, trimTrailingSlash(webUrl), trimTrailingSlash(apiUrl), @@ -482,6 +547,10 @@ public Server normalized() { ); } + private String authorizationPrefix() { + return isGitea() ? "token " : "Bearer "; + } + private String key() { final Server normalized = normalized(); return normalized.type + "|" + normalized.webUrl + "|" + normalized.apiUrl; @@ -490,7 +559,8 @@ private String key() { public static class Authorizations { - private static final List DEFAULT_ENV_TOKENS = List.of("GITHUB_TOKEN", "GH_TOKEN", "GITHUB_PAT"); + private static final List DEFAULT_GITHUB_ENV_TOKENS = List.of("GITHUB_TOKEN", "GH_TOKEN", "GITHUB_PAT"); + private static final List DEFAULT_GITEA_ENV_TOKENS = List.of("GITEA_TOKEN", "GITEA_PAT"); public static List forApiUrl(final String apiUrl, final String tokenEnvVar, final Project project) { return forApiUrl(apiUrl, tokenEnvVar, project, System.getenv()); @@ -501,13 +571,91 @@ public static List forApiUrl( final String tokenEnvVar, final Project project, final Map environment + ) { + return forGithubApiUrl(apiUrl, tokenEnvVar, project, environment); + } + + /** + * Returns authorizations for a workflow-run request. Gitea run requests use Gitea token variables with + * {@code token ...}; GitHub requests keep JetBrains GitHub account and {@code Bearer ...} token priority. + * + * @param apiUrl REST API URL configured for the run + * @param workflowPath workflow file path being dispatched + * @param tokenEnvVar optional token environment variable configured for the run + * @param project project used when JetBrains GitHub tokens need to be requested + * @return ordered authorizations ending with anonymous access + */ + public static List forWorkflowRun( + final String apiUrl, + final String workflowPath, + final String tokenEnvVar, + final Project project + ) { + return forWorkflowRun(apiUrl, workflowPath, tokenEnvVar, project, System.getenv()); + } + + /** + * Returns authorizations for a workflow-run request using the supplied environment map. + * + * @param apiUrl REST API URL configured for the run + * @param workflowPath workflow file path being dispatched + * @param tokenEnvVar optional token environment variable configured for the run + * @param project project used when JetBrains GitHub tokens need to be requested + * @param environment environment variables used for explicit and default tokens + * @return ordered authorizations ending with anonymous access + */ + public static List forWorkflowRun( + final String apiUrl, + final String workflowPath, + final String tokenEnvVar, + final Project project, + final Map environment + ) { + return forServer(Server.fromWorkflowRun(apiUrl, workflowPath, tokenEnvVar), project, environment); + } + + public static List forServer(final Server server, final Project project) { + return forServer(server, project, System.getenv()); + } + + /** + * Returns authorizations for a configured remote provider. GitHub providers reuse JetBrains GitHub accounts + * and GitHub token environment variables. Gitea providers use only configured or Gitea-specific environment + * tokens because JetBrains has no compatible Gitea account store here. + * + * @param server configured remote provider + * @param project project used when JetBrains GitHub tokens need to be requested + * @param environment environment variables used for explicit and default tokens + * @return ordered authorizations ending with anonymous access + */ + public static List forServer( + final Server server, + final Project project, + final Map environment + ) { + final Server normalized = Optional.ofNullable(server).orElseGet(Settings::defaultGitHub).normalized(); + if (normalized.isGitea()) { + final LinkedHashMap result = new LinkedHashMap<>(); + envAuthorizations(normalized.tokenEnvVar, environment, DEFAULT_GITEA_ENV_TOKENS, "token ") + .forEach(authorization -> result.putIfAbsent(authorization.key(), authorization)); + result.putIfAbsent(Authorization.anonymous().key(), Authorization.anonymous()); + return List.copyOf(result.values()); + } + return forGithubApiUrl(normalized.apiUrl, normalized.tokenEnvVar, project, environment); + } + + private static List forGithubApiUrl( + final String apiUrl, + final String tokenEnvVar, + final Project project, + final Map environment ) { final LinkedHashMap result = new LinkedHashMap<>(); orderedAccountsFor(apiUrl).stream() .map(account -> authorization(account, project)) .flatMap(Optional::stream) .forEach(authorization -> result.putIfAbsent(authorization.key(), authorization)); - envAuthorizations(tokenEnvVar, environment) + envAuthorizations(tokenEnvVar, environment, DEFAULT_GITHUB_ENV_TOKENS, "Bearer ") .forEach(authorization -> result.putIfAbsent(authorization.key(), authorization)); result.putIfAbsent(Authorization.anonymous().key(), Authorization.anonymous()); return List.copyOf(result.values()); @@ -551,24 +699,34 @@ private static Optional authorization(final GithubAccount account } } - private static List envAuthorizations(final String tokenEnvVar, final Map environment) { + private static List envAuthorizations( + final String tokenEnvVar, + final Map environment, + final List defaultTokenEnvVars, + final String authorizationPrefix + ) { final LinkedHashMap result = new LinkedHashMap<>(); - envAuthorization(tokenEnvVar, environment).ifPresent(authorization -> result.putIfAbsent(authorization.key(), authorization)); - DEFAULT_ENV_TOKENS.stream() + final Map values = Optional.ofNullable(environment).orElseGet(Map::of); + envAuthorization(tokenEnvVar, values, authorizationPrefix).ifPresent(authorization -> result.putIfAbsent(authorization.key(), authorization)); + defaultTokenEnvVars.stream() .filter(name -> !name.equals(tokenEnvVar)) - .map(name -> envAuthorization(name, environment)) + .map(name -> envAuthorization(name, values, authorizationPrefix)) .flatMap(Optional::stream) .forEach(authorization -> result.putIfAbsent(authorization.key(), authorization)); return List.copyOf(result.values()); } - private static Optional envAuthorization(final String tokenEnvVar, final Map environment) { + private static Optional envAuthorization( + final String tokenEnvVar, + final Map environment, + final String authorizationPrefix + ) { return Optional.ofNullable(tokenEnvVar) .map(String::trim) .filter(RemoteActionProviders::hasText) .flatMap(name -> Optional.ofNullable(environment.get(name)) .filter(RemoteActionProviders::hasText) - .map(token -> new Authorization(name, "Bearer " + token))); + .map(token -> new Authorization(name, authorizationPrefix + token))); } private static Project project(final Project project) { @@ -617,6 +775,26 @@ private static String trimTrailingSlash(final String value) { return trimmed.endsWith("/") ? trimmed.substring(0, trimmed.length() - 1) : trimmed; } + private static String webUrlFromApiUrl(final String apiUrl) { + final String trimmed = trimTrailingSlash(apiUrl); + if (trimmed.equals("https://api.github.com")) { + return "https://github.com"; + } + return trimmed.replaceFirst("/api/v(?:1|3)$", ""); + } + + private static boolean looksLikeGitea( + final String apiUrl, + final String workflowPath, + final String tokenEnvVar + ) { + final String normalizedWorkflowPath = Optional.ofNullable(workflowPath).orElse("").replace('\\', '/'); + final String normalizedTokenEnvVar = Optional.ofNullable(tokenEnvVar).orElse("").toUpperCase(); + return normalizedWorkflowPath.startsWith(".gitea/workflows/") + || trimTrailingSlash(apiUrl).endsWith("/api/v1") + || normalizedTokenEnvVar.contains("GITEA"); + } + private static boolean hasText(final String value) { return value != null && !value.isBlank(); } diff --git a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java index 397ac53..3ff5def 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java @@ -69,11 +69,21 @@ public WorkflowRun(final Project project) { this(new JdkHttpTransport(HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NORMAL) - .build()), request -> RemoteActionProviders.Authorizations.forApiUrl(request.apiUrl(), request.tokenEnvVar(), project)); + .build()), request -> RemoteActionProviders.Authorizations.forWorkflowRun( + request.apiUrl(), + request.workflowPath(), + request.tokenEnvVar(), + project + )); } WorkflowRun(final HttpTransport transport) { - this(transport, request -> RemoteActionProviders.Authorizations.forApiUrl(request.apiUrl(), request.tokenEnvVar(), null)); + this(transport, request -> RemoteActionProviders.Authorizations.forWorkflowRun( + request.apiUrl(), + request.workflowPath(), + request.tokenEnvVar(), + null + )); } WorkflowRun(final HttpTransport transport, final AuthorizationProvider authorizationProvider) { @@ -85,7 +95,7 @@ public DispatchResult dispatch(final Request request) throws IOException, Interr final HttpResponse response = send( request, "POST", - workflowUrl(request) + "/dispatches", + dispatchUrl(request), dispatchBody(request), "GitHub workflow dispatch" ); @@ -169,7 +179,7 @@ public Optional latestRun(final Request request) throws IOException, final HttpResponse response = send( request, "GET", - workflowUrl(request) + "/runs?branch=" + encode(request.ref()) + "&event=workflow_dispatch&per_page=1", + latestRunsUrl(request), "", "GitHub workflow run discovery" ); @@ -363,9 +373,13 @@ private static HttpRequest request( ) { final HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url)) .timeout(TIMEOUT) - .header("Accept", "application/vnd.github+json") - .header("X-GitHub-Api-Version", API_VERSION) .header("User-Agent", "GitHub-Workflow-Plugin"); + if (server(workflow).isGitea()) { + builder.header("Accept", "application/json"); + } else { + builder.header("Accept", "application/vnd.github+json"); + builder.header("X-GitHub-Api-Version", API_VERSION); + } if (authorization.authenticated()) { builder.header("Authorization", authorization.authorizationHeader()); } @@ -473,6 +487,20 @@ private static String workflowUrl(final Request request) { return request.apiUrl() + "/repos/" + encode(request.owner()) + "/" + encode(request.repo()) + "/actions/workflows/" + encode(workflowId(request.workflowPath())); } + private static String dispatchUrl(final Request request) { + final String url = workflowUrl(request) + "/dispatches"; + return server(request).isGitea() ? url + "?return_run_details=true" : url; + } + + private static String latestRunsUrl(final Request request) { + final RemoteActionProviders.Server server = server(request); + final String baseUrl = server.isGitea() + ? request.apiUrl() + "/repos/" + encode(request.owner()) + "/" + encode(request.repo()) + "/actions/runs" + : workflowUrl(request) + "/runs"; + final String pageLimit = server.isGitea() ? "limit=1" : "per_page=1"; + return baseUrl + "?branch=" + encode(request.ref()) + "&event=workflow_dispatch&" + pageLimit; + } + private static String authorizationCacheKey(final Request request) { return Optional.ofNullable(request.apiUrl()).orElse("") + "|" + Optional.ofNullable(request.tokenEnvVar()).orElse(""); } @@ -487,6 +515,10 @@ private static String workflowId(final String workflowPath) { return slash < 0 ? normalized : normalized.substring(slash + 1); } + private static RemoteActionProviders.Server server(final Request request) { + return RemoteActionProviders.Server.fromWorkflowRun(request.apiUrl(), request.workflowPath(), request.tokenEnvVar()); + } + private static String dispatchBody(final Request request) { final StringJoiner inputs = new StringJoiner(","); request.inputs().entrySet().stream() diff --git a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java index 3d752ef..db2966d 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java @@ -341,9 +341,9 @@ private String dispatchMessage() { } private String workflowUrl() { - final String webUrl = request.apiUrl().equals("https://api.github.com") - ? "https://github.com" - : request.apiUrl().replaceFirst("/api/v3/?$", ""); + final String webUrl = RemoteActionProviders.Server + .fromWorkflowRun(request.apiUrl(), request.workflowPath(), request.tokenEnvVar()) + .webUrl; return " (" + webUrl + "/" + request.owner() + "/" + request.repo() + "/blob/" + request.ref() + "/" + request.workflowPath() + ")"; } diff --git a/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java b/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java new file mode 100644 index 0000000..a0e4574 --- /dev/null +++ b/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java @@ -0,0 +1,212 @@ +package com.github.yunabraska.githubworkflow.git; + +import com.github.yunabraska.githubworkflow.model.GitHubAction; +import com.google.gson.JsonObject; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GiteaDockerIntegrationTest extends BasePlatformTestCase { + + private static final String TEST_SWITCH = "GITEA_DOCKER_TEST"; + private static final String IMAGE = Optional.ofNullable(System.getenv("GITEA_IMAGE")) + .filter(value -> !value.isBlank()) + .orElse("docker.gitea.com/gitea:1.26.2-rootless"); + private static final HttpClient CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(3)) + .build(); + + @Override + protected void tearDown() throws Exception { + try { + RemoteActionProviders.Settings.getInstance().setCustomServers(List.of()); + } finally { + super.tearDown(); + } + } + + public void testGiteaApiV1ResolvesActionsAndWorkflowsFromEmbeddedContainer() throws Exception { + if ("false".equalsIgnoreCase(System.getenv(TEST_SWITCH))) { + return; + } + try (GiteaContainer gitea = GiteaContainer.start()) { + final String token = gitea.createAdminToken(); + gitea.createRepository(token, "action-box"); + gitea.createFile(token, "action-box", "action.yml", """ + name: Action Box + inputs: + flavor: + description: Test flavor + outputs: + artifact: + description: Test artifact + runs: + using: composite + steps: + - run: echo ok + shell: sh + """); + gitea.createFile(token, "action-box", ".gitea/workflows/reuse.yml", """ + name: Reuse + on: + workflow_call: + inputs: + config: + type: string + jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo ok + """); + RemoteActionProviders.Settings.getInstance().setCustomServers(List.of(RemoteActionProviders.Server.gitea( + "Embedded Gitea", + gitea.webUrl(), + gitea.apiUrl(), + "", + true + ))); + + final GitHubAction action = GitHubAction.createGithubAction(false, gitea.webUrl() + "/test/action-box@main", "gitea-action").resolve(); + final String workflowUses = gitea.webUrl() + "/test/action-box/.gitea/workflows/reuse.yml@main"; + final GitHubAction workflow = GitHubAction.createGithubAction(false, workflowUses, "gitea-workflow").resolve(); + + assertThat(action.isResolved()).isTrue(); + assertThat(action.isAction()).isTrue(); + assertThat(action.freshInputs()).containsKey("flavor"); + assertThat(action.freshOutputs()).containsKey("artifact"); + assertThat(action.remoteRefs()).contains("main"); + assertThat(workflow.isResolved()).isTrue(); + assertThat(workflow.isAction()).isFalse(); + assertThat(workflow.freshInputs()).containsKey("config"); + assertThat(workflow.githubUrl()).isEqualTo(gitea.webUrl() + "/test/action-box/blob/main/.gitea/workflows/reuse.yml"); + } + } + + private record GiteaContainer(String id, String webUrl) implements AutoCloseable { + + static GiteaContainer start() throws Exception { + final String id = run("docker", "run", "--rm", "-d", + "-p", "127.0.0.1::3000", + "-e", "GITEA__database__DB_TYPE=sqlite3", + "-e", "GITEA__database__PATH=/var/lib/gitea/gitea.db", + "-e", "GITEA__security__INSTALL_LOCK=true", + "-e", "GITEA__service__DISABLE_REGISTRATION=true", + "-e", "GITEA__server__HTTP_PORT=3000", + IMAGE + ).trim(); + final String port = run("docker", "port", id, "3000/tcp").trim().replaceFirst(".*:", ""); + final GiteaContainer container = new GiteaContainer(id, "http://127.0.0.1:" + port); + container.waitUntilReady(); + return container; + } + + String apiUrl() { + return webUrl + "/api/v1"; + } + + String createAdminToken() throws Exception { + run("docker", "exec", id, "gitea", "admin", "user", "create", + "--username", "test", + "--password", "test-password", + "--email", "test@example.com", + "--admin", + "--must-change-password=false" + ); + final String output = run("docker", "exec", id, "gitea", "admin", "user", "generate-access-token", + "--username", "test", + "--token-name", "plugin-test", + "--scopes", "all" + ); + return Pattern.compile("created:\\s*(\\S+)") + .matcher(output) + .results() + .map(result -> result.group(1)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Gitea token was not printed")); + } + + void createRepository(final String token, final String name) throws Exception { + final JsonObject body = new JsonObject(); + body.addProperty("name", name); + body.addProperty("auto_init", true); + post(token, "/user/repos", body); + } + + void createFile(final String token, final String repo, final String path, final String content) throws Exception { + final JsonObject body = new JsonObject(); + body.addProperty("branch", "main"); + body.addProperty("message", "add " + path); + body.addProperty("content", Base64.getEncoder().encodeToString(content.getBytes(StandardCharsets.UTF_8))); + post(token, "/repos/test/" + encode(repo) + "/contents/" + path, body); + } + + private void post(final String token, final String path, final JsonObject body) throws Exception { + final HttpRequest request = HttpRequest.newBuilder(URI.create(apiUrl() + path)) + .timeout(Duration.ofSeconds(10)) + .header("Authorization", "token " + token) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body.toString(), StandardCharsets.UTF_8)) + .build(); + final HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() / 100 != 2) { + throw new IllegalStateException("Gitea API failed with HTTP " + response.statusCode() + ": " + response.body()); + } + } + + private void waitUntilReady() throws Exception { + for (int attempt = 0; attempt < 30; attempt++) { + try { + final HttpRequest request = HttpRequest.newBuilder(URI.create(apiUrl() + "/version")) + .timeout(Duration.ofSeconds(2)) + .GET() + .build(); + if (CLIENT.send(request, HttpResponse.BodyHandlers.discarding()).statusCode() == 200) { + return; + } + } catch (final IOException ignored) { + // Gitea is still waking up. + } + Thread.sleep(1_000); + } + throw new IllegalStateException("Gitea did not become ready"); + } + + @Override + public void close() throws Exception { + run("docker", "stop", id); + } + } + + private static String encode(final String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20"); + } + + private static String run(final String... command) throws IOException, InterruptedException { + final Process process = new ProcessBuilder(command) + .redirectErrorStream(true) + .start(); + final String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + if (!process.waitFor(60, java.util.concurrent.TimeUnit.SECONDS)) { + process.destroyForcibly(); + throw new IOException("Command timed out: " + String.join(" ", command)); + } + if (process.exitValue() != 0) { + throw new IOException("Command failed [" + String.join(" ", command) + "]: " + output); + } + return output; + } +} diff --git a/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java b/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java index 55a8f3e..d40dc90 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java @@ -175,6 +175,34 @@ public void testGithubReusableWorkflowPathIsDetectedAsWorkflowMetadata() throws } } + public void testGiteaReusableWorkflowPathIsDetectedAsWorkflowMetadata() throws Exception { + try (FakeRemoteServer server = new FakeRemoteServer()) { + server.addContent("acme", "automation", ".gitea/workflows/reuse.yml", "main", """ + name: Reuse + on: + workflow_call: + inputs: + config: + type: string + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo ok + """); + useGiteaServer(server, ""); + + final String usesValue = server.webUrl() + "/acme/automation/.gitea/workflows/reuse.yml@main"; + final GitHubAction action = GitHubAction.createGithubAction(false, usesValue, server.webUrl()).resolve(); + + assertThat(action.isResolved()).isTrue(); + assertThat(action.isAction()).isFalse(); + assertThat(action.freshInputs()).containsKey("config"); + assertThat(action.githubUrl()).isEqualTo(server.webUrl() + "/acme/automation/blob/main/.gitea/workflows/reuse.yml"); + assertThat(server.requests()).contains("/api/v1/repos/acme/automation/contents/.gitea/workflows/reuse.yml?ref=main"); + } + } + public void testLatestRefsReturnsLatestTenTagsBeforeBranches() throws Exception { try (FakeRemoteServer server = new FakeRemoteServer()) { server.setTags("acme", "tool", List.of("v10", "v9", "v8", "v7", "v6", "v5", "v4", "v3", "v2", "v1", "v0")); @@ -232,6 +260,69 @@ public void testStandardEnvironmentTokensAreTriedBeforeAnonymous() { .contains("Bearer env-token"); } + public void testGiteaEnvironmentTokensUseTokenAuthorizationScheme() { + final RemoteActionProviders.Server server = RemoteActionProviders.Server.gitea( + "Local Gitea", + "http://gitea.local", + "http://gitea.local/api/v1", + "LOCAL_GITEA_TOKEN", + true + ); + + final List authorizations = RemoteActionProviders.Authorizations.forServer( + server, + null, + Map.of("LOCAL_GITEA_TOKEN", "local-token", "GITEA_TOKEN", "default-token", "GITHUB_TOKEN", "wrong-for-gitea") + ); + + assertThat(authorizations) + .extracting(RemoteActionProviders.Authorizations.Authorization::source) + .containsExactly("LOCAL_GITEA_TOKEN", "GITEA_TOKEN", "anonymous"); + assertThat(authorizations) + .extracting(RemoteActionProviders.Authorizations.Authorization::authorizationHeader) + .containsExactly("token local-token", "token default-token", ""); + } + + public void testGiteaWorkflowRunEnvironmentTokensUseTokenAuthorizationScheme() { + final List authorizations = RemoteActionProviders.Authorizations.forWorkflowRun( + "http://gitea.local/api/v1", + ".gitea/workflows/build.yml", + "LOCAL_GITEA_TOKEN", + null, + Map.of("LOCAL_GITEA_TOKEN", "local-token", "GITEA_TOKEN", "default-token", "GITHUB_TOKEN", "wrong-for-gitea") + ); + + assertThat(authorizations) + .extracting(RemoteActionProviders.Authorizations.Authorization::source) + .containsExactly("LOCAL_GITEA_TOKEN", "GITEA_TOKEN", "anonymous"); + assertThat(authorizations) + .extracting(RemoteActionProviders.Authorizations.Authorization::authorizationHeader) + .containsExactly("token local-token", "token default-token", ""); + } + + public void testGithubEnvironmentTokensStillUseBearerAuthorizationScheme() { + final RemoteActionProviders.Server server = new RemoteActionProviders.Server( + "GitHub", + "https://github.com", + "https://api.github.com", + "LOCAL_GITHUB_TOKEN", + true + ); + + final List authorizations = RemoteActionProviders.Authorizations.forServer( + server, + null, + Map.of("LOCAL_GITHUB_TOKEN", "local-token", "GITHUB_TOKEN", "default-token", "GITEA_TOKEN", "wrong-for-github") + ); + + assertThat(authorizations) + .extracting(RemoteActionProviders.Authorizations.Authorization::source) + .containsSubsequence("LOCAL_GITHUB_TOKEN", "GITHUB_TOKEN", "anonymous"); + assertThat(authorizations) + .extracting(RemoteActionProviders.Authorizations.Authorization::authorizationHeader) + .contains("Bearer local-token", "Bearer default-token"); + } + public void testExplicitEnvironmentTokenIsTriedBeforeStandardEnvironmentTokens() { final List authorizations = RemoteActionProviders.Authorizations.forApiUrl( "https://github.acme.test/api/v3", @@ -267,4 +358,14 @@ private static void useServer(final FakeRemoteServer server, final String apiPre true ))); } + + private static void useGiteaServer(final FakeRemoteServer server, final String tokenEnvVar) { + RemoteActionProviders.Settings.getInstance().setCustomServers(List.of(RemoteActionProviders.Server.gitea( + "Fake Gitea", + server.webUrl(), + server.apiUrl("/api/v1"), + tokenEnvVar, + true + ))); + } } diff --git a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandlerTest.java b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandlerTest.java index 5529a5e..50c818b 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandlerTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandlerTest.java @@ -415,6 +415,51 @@ public void processTerminated(@NotNull final ProcessEvent event) { .contains("feature/live-logs"); } + public void testProcessUsesGiteaWorkflowUrlInDispatchMessage() throws Exception { + final AtomicInteger statusCalls = new AtomicInteger(0); + final AtomicInteger jobCalls = new AtomicInteger(0); + final AtomicInteger logCalls = new AtomicInteger(0); + final WorkflowRun client = new WorkflowRun( + request -> responseFor(request, statusCalls, jobCalls, logCalls), + request -> List.of(RemoteActionProviders.Authorizations.Authorization.anonymous()) + ); + final WorkflowRun.Request request = new WorkflowRun.Request( + "http://localhost:3000/api/v1", + "basuabhi", + "demo-app", + ".gitea/workflows/build-update-chart.yaml", + "main", + Map.of(), + "GITEA_TOKEN" + ); + final WorkflowRunProcessHandler handler = new WorkflowRunProcessHandler( + getProject(), + request, + client, + new WorkflowRunProcessHandler.PollSettings(10, 10, 10) + ); + final CountDownLatch terminated = new CountDownLatch(1); + final StringBuilder output = new StringBuilder(); + handler.addProcessListener(new ProcessListener() { + @Override + public void onTextAvailable(@NotNull final ProcessEvent event, @NotNull final com.intellij.openapi.util.Key outputType) { + output.append(event.getText()); + } + + @Override + public void processTerminated(@NotNull final ProcessEvent event) { + terminated.countDown(); + } + }); + + handler.startNotify(); + + assertThat(terminated.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(output.toString()) + .contains("http://localhost:3000/basuabhi/demo-app/blob/main/.gitea/workflows/build-update-chart.yaml") + .doesNotContain("http://localhost:3000/api/v1/basuabhi/demo-app/blob"); + } + public void testProcessRetriesLiveLogAfterDeferredHttpFailure() throws Exception { final AtomicInteger statusCalls = new AtomicInteger(0); final AtomicInteger logCalls = new AtomicInteger(0); diff --git a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java index 8173d95..f6b204d 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java @@ -106,6 +106,33 @@ public void testLatestRunDiscoversNewestWorkflowDispatchRun() throws Exception { } } + public void testGiteaDispatchRequestsRunDetailsAndLatestRunUsesRepositoryRunsEndpoint() throws Exception { + try (FakeWorkflowRunServer server = new FakeWorkflowRunServer()) { + final WorkflowRun client = new WorkflowRun(); + final WorkflowRun.Request request = new WorkflowRun.Request( + server.apiUrl() + "/api/v1", + "acme", + "tool", + ".gitea/workflows/build-update-chart.yaml", + "main", + Map.of(), + "GITEA_TOKEN" + ); + + final WorkflowRun.DispatchResult dispatch = client.dispatch(request); + final var latest = client.latestRun(request); + + assertThat(dispatch.runId()).isEqualTo(42); + assertThat(latest).isPresent(); + assertThat(latest.get().runId()).isEqualTo(88); + assertThat(server.requests()).contains( + "/api/v1/repos/acme/tool/actions/workflows/build-update-chart.yaml/dispatches?return_run_details=true", + "/api/v1/repos/acme/tool/actions/runs?branch=main&event=workflow_dispatch&limit=1" + ); + assertThat(server.requests()).noneMatch(path -> path.contains("/workflows/build-update-chart.yaml/runs")); + } + } + public void testArtifactsAndZipUseRunArtifactEndpoints() throws Exception { try (FakeWorkflowRunServer server = new FakeWorkflowRunServer()) { final WorkflowRun client = new WorkflowRun(); @@ -369,6 +396,9 @@ private Response responseFor(final String path) { if (path.endsWith("/workflows/build.yml/runs")) { return new Response(200, "application/json", "{\"workflow_runs\":[{\"id\":77,\"status\":\"queued\",\"conclusion\":null,\"html_url\":\"html-latest\"}]}"); } + if (path.endsWith("/actions/runs")) { + return new Response(200, "application/json", "{\"workflow_runs\":[{\"id\":88,\"status\":\"queued\",\"conclusion\":null,\"html_url\":\"gitea-latest\"}]}"); + } if (path.endsWith("/runs/42/jobs")) { return new Response(200, "application/json", "{\"jobs\":[{\"id\":100,\"name\":\"build\",\"status\":\"completed\",\"conclusion\":\"success\"}]}"); } From 6ba2c6b5b9c859a4b6cc36fc598407b98ee716e2 Mon Sep 17 00:00:00 2001 From: Yuna Morgenstern Date: Thu, 11 Jun 2026 07:36:01 +0200 Subject: [PATCH 2/8] Harden Gitea workflow syntax --- .github/workflows/build.yml | 6 + CHANGELOG.md | 1 + README.md | 2 +- doc/spec/editor-test-matrix.md | 2 + .../gitea-github-actions-compatibility.md | 5 +- .../entry/WorkflowAnnotator.java | 25 ++- .../entry/WorkflowCompletion.java | 44 +++-- .../githubworkflow/syntax/WorkflowSyntax.java | 181 +++++++++++++++--- .../githubworkflow/syntax/WorkflowYaml.java | 22 +++ .../resources/github-docs/workflow-syntax.tsv | 16 ++ .../syntax/WorkflowMetadataTest.java | 4 + .../syntax/WorkflowSyntaxCompletionTest.java | 71 +++++++ .../syntax/WorkflowValidationTest.java | 33 ++++ .../test/EditorFeatureTestCase.java | 22 ++- 14 files changed, 382 insertions(+), 52 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77b7020..97ebf1a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,6 +145,12 @@ jobs: distribution: temurin java-version: 25 + - name: 🐳 Check Docker + shell: sh + run: | + docker version + docker info + - name: 🏷️ Prepare release metadata if: steps.plan.outputs.release == 'true' shell: sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a0b4b6..a15999d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Fixed the README build badge link and refreshed navigation/release docs. - `.gitea/workflows/*` files now get their own light/dark Gitea-flavored file icon instead of cosplaying GitHub. - Gitea workflow runs now use `GITEA_TOKEN`-style auth, clean browser links, and repo-level run discovery for `/api/v1`. +- `.gitea/workflows/*` syntax now uses Gitea permission scopes and cron aliases instead of GitHub-only scope clutter. ## [2026.5.29] - 2026-05-29 diff --git a/README.md b/README.md index 1db905f..ceb7a27 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ _[See Screenshots](https://plugins.jetbrains.com/plugin/21396-github-workflow)_ matching IDE accounts first, then other IDE GitHub accounts, then `GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_PAT`, then anonymous access. An optional token environment variable can still be set explicitly for custom setups. GitHub log timestamps, groups, command markers, and ANSI color codes are compacted before display. -* **Usage**: Enjoy autocomplete, syntax highlighting, and much more as you code your GitHub Workflows and Actions. +* **Usage**: Enjoy autocomplete, syntax highlighting, and much more as you code GitHub or Gitea Workflows and Actions. ## Local Development diff --git a/doc/spec/editor-test-matrix.md b/doc/spec/editor-test-matrix.md index 51798b4..df97535 100644 --- a/doc/spec/editor-test-matrix.md +++ b/doc/spec/editor-test-matrix.md @@ -58,6 +58,8 @@ Official syntax references: `runner.*`, including `runner.debug`, `job.*`, nested `job.container.*`, strict local `job.services.*`, `strategy.*`, `matrix.*`, and unknown external `vars.*` contexts. - `gitea.*` context highlighting and completion uses the same key map as `github.*`. +- `.gitea/workflows/*` syntax uses Gitea token permission scopes, Gitea permission shorthand values, and Gitea cron + alias completion without leaking GitHub-only permission scopes from the bundled GitHub schema. - `job.services.` validation/completion/reference/styling from local job service definitions, including `id`, `network`, `ports`, and mapped port keys. - Matrix keys from direct `strategy.matrix` entries and `strategy.matrix.include`. - `steps..outputs.` validation and completion for previous run outputs, multiline `$GITHUB_OUTPUT`, `tee -a $GITHUB_OUTPUT`, and resolved action outputs. diff --git a/doc/spec/gitea-github-actions-compatibility.md b/doc/spec/gitea-github-actions-compatibility.md index 9a3c2d7..edd0770 100644 --- a/doc/spec/gitea-github-actions-compatibility.md +++ b/doc/spec/gitea-github-actions-compatibility.md @@ -32,16 +32,17 @@ shared behavior first and branch only where Gitea really differs. Tiny forks, no | Artifacts | Run artifact list plus artifact ZIP | Same core paths in Gitea OpenAPI | Existing artifact list/download path is shared. | | Context root | `github.*` | `gitea.*`, with `github.*` as alias | Completion/highlighting covers `gitea.*` using the GitHub-compatible key map. | | Absolute action URLs | GitHub syntax is usually `owner/repo[/path]@ref` | Absolute URLs are supported | Remote resolver supports absolute URLs for configured servers. | +| Cron aliases | GitHub uses POSIX cron expressions | Gitea also accepts `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly` | `.gitea/workflows` cron completion suggests the Gitea aliases. | +| Permission scopes | GitHub has GitHub-only scopes such as `id-token`, `statuses`, `pages` | Gitea has `code`, `releases`, `wiki`, `projects`, and a smaller shared scope set | `.gitea/workflows` completion and validation use the Gitea scope set. | +| Permission shorthand | GitHub completion includes `read-all`, `write-all`, and `{}` | Gitea documents `read-all` and `write-all` scalar values | `.gitea/workflows` shorthand completion stays on documented Gitea values. | ## Differences To Keep Watching | Area | Gitea behavior | Plugin risk | Suggested handling | | --- | --- | --- | --- | -| Non-standard cron aliases | Gitea supports `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`; GitHub does not. | GitHub-style validation may mark valid Gitea schedules as odd. | Add Gitea-aware completion/validation once workflow provider detection is available in syntax checks. | | Ignored job keys | Gitea currently ignores `jobs..timeout-minutes`, `continue-on-error`, and `environment`. | Warnings should not imply Gitea will enforce these keys. | Keep syntax valid, optionally show Gitea-specific weak info later. | | Complex `runs-on` | Gitea supports scalar or single-item array forms, not GitHub's richer runner targeting. | Completion may suggest shapes Gitea does not execute. | Keep GitHub completion by default; add Gitea-aware validation only for `.gitea/workflows`. | | Expressions | Gitea comparison says only `always()` is supported from expression functions. | GitHub-rich expression completion can overpromise in Gitea files. | Treat as future Gitea-specific inspection, not a parser fork. | -| Permission scopes | Gitea supports a subset plus `code`, `releases`, `wiki`, `projects`; GitHub supports scopes such as `statuses`, `checks`, `deployments`, `id-token`, `security-events`, and `pages`. | Current permission completion is GitHub-shaped. | Add provider-aware permission scopes for `.gitea/workflows`. | | Problem matchers and annotations | Gitea ignores problem matchers and workflow command annotations. | Log rendering can still color warnings/errors locally; remote UI may not. | No code change needed unless we add Gitea-specific docs. | | Default action source | Gitea may resolve unqualified `uses:` through instance config (`github` or `self`). | Plugin cannot know server admin config. | Keep configured-server absolute URL support; do not guess admin config. | | Secret and variable names | Gitea disallows user-created names starting with `GITHUB_` or `GITEA_`; variables are uppercased. | Future settings/UI for external variables must enforce Gitea naming rules. | Documented only; no external variable CRUD exists. | diff --git a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java index cc89dd6..07b8e60 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java @@ -196,7 +196,12 @@ private static void validateWorkflowSyntax(final AnnotationHolder holder, final } WorkflowLocation.from(psiElement) .filter(WorkflowAnnotator::shouldValidateWorkflowSyntax) - .ifPresent(location -> validateWorkflowKeyValue(holder, location.keyValue(), location.path())); + .ifPresent(location -> validateWorkflowKeyValue( + holder, + location.keyValue(), + location.path(), + WorkflowSyntax.providerFor(location.keyValue()) + )); } private static boolean shouldValidateWorkflowSyntax(final WorkflowLocation location) { @@ -208,17 +213,23 @@ private static boolean isUnitTestWorkflowFile(final YAMLKeyValue element) { && WorkflowPsi.getChild(element.getContainingFile(), "runs").isEmpty(); } - private static void validateWorkflowKeyValue(final AnnotationHolder holder, final YAMLKeyValue element, final List path) { - WorkflowSyntax.validationKeysForPath(path).ifPresent(keys -> { + private static void validateWorkflowKeyValue( + final AnnotationHolder holder, + final YAMLKeyValue element, + final List path, + final WorkflowSyntax.Provider provider + ) { + WorkflowSyntax.validationKeysForPath(path, provider).ifPresent(keys -> { validateKnownKey(holder, element, keys.values(), keys.messageKey()); - validateWorkflowPropertyValue(holder, element, path); + validateWorkflowPropertyValue(holder, element, path, provider); }); } private static void validateWorkflowPropertyValue( final AnnotationHolder holder, final YAMLKeyValue element, - final List path + final List path, + final WorkflowSyntax.Provider provider ) { final String key = element.getKeyText(); if (isChildOf(path, FIELD_ON, "workflow_dispatch", FIELD_INPUTS) @@ -229,10 +240,10 @@ private static void validateWorkflowPropertyValue( validateKnownValue(holder, element, WorkflowSyntax.booleanValues(), "inspection.workflow.syntax.unknownTriggerValue"); } if (pathMatches(path, FIELD_ON, "*") && "types".equals(key)) { - validateKnownValue(holder, element, WorkflowSyntax.eventActivityTypesFor(path.get(1)), "inspection.workflow.syntax.unknownTriggerValue"); + validateKnownValue(holder, element, WorkflowSyntax.eventActivityTypesFor(path.get(1), provider), "inspection.workflow.syntax.unknownTriggerValue"); } if (pathEndsWith(path, "permissions")) { - validateKnownValue(holder, element, WorkflowSyntax.permissionValuesFor(key), "inspection.workflow.syntax.unknownPermissionValue"); + validateKnownValue(holder, element, WorkflowSyntax.permissionValuesFor(key, provider), "inspection.workflow.syntax.unknownPermissionValue"); } } diff --git a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java index d052f6f..1a79b70 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java @@ -355,15 +355,16 @@ private static Optional workflowStructureCompletion(final C return Optional.empty(); } final List path = context.get().path(); - final Optional> keys = WorkflowSyntax.completionKeysForPath(path); + final WorkflowSyntax.Provider provider = WorkflowSyntax.providerFor(completionPsi.position()); + final Optional> keys = WorkflowSyntax.completionKeysForPath(path, provider); if (keys.isPresent()) { return Optional.of(new StructureCompletion(keys.get(), ':')); } if (pathMatches(path, FIELD_ON, "*", "types")) { - return Optional.of(new StructureCompletion(WorkflowSyntax.eventActivityTypesFor(path.get(1)), Character.MIN_VALUE)); + return Optional.of(new StructureCompletion(WorkflowSyntax.eventActivityTypesFor(path.get(1), provider), Character.MIN_VALUE)); } if (pathMatches(path, FIELD_ON, "*", "*")) { - return workflowEventFilterValueCompletion(completionPsi, context.get()); + return workflowEventFilterValueCompletion(completionPsi, context.get(), provider); } return Optional.empty(); } @@ -381,19 +382,20 @@ private static Optional workflowValueCompletion(final Compl return Optional.empty(); } final List path = context.get().path(); + final WorkflowSyntax.Provider provider = WorkflowSyntax.providerFor(completionPsi.position()); final String currentKey = key.get(); - final Optional eventFilterValueCompletion = workflowEventFilterValueCompletion(completionPsi, context.get()); + final Optional eventFilterValueCompletion = workflowEventFilterValueCompletion(completionPsi, context.get(), provider); if (eventFilterValueCompletion.isPresent()) { return eventFilterValueCompletion; } if ("runs-on".equals(currentKey)) { - return Optional.of(new StructureCompletion(WorkflowSyntax.runnerLabels(), Character.MIN_VALUE)); + return Optional.of(new StructureCompletion(WorkflowSyntax.runnerLabels(provider), Character.MIN_VALUE)); } if ("permissions".equals(currentKey)) { - return Optional.of(new StructureCompletion(WorkflowSyntax.permissionShorthandValues(), Character.MIN_VALUE)); + return Optional.of(new StructureCompletion(WorkflowSyntax.permissionShorthandValues(provider), Character.MIN_VALUE)); } if ("types".equals(currentKey) && pathMatches(path, FIELD_ON, "*")) { - return Optional.of(new StructureCompletion(WorkflowSyntax.eventActivityTypesFor(path.get(1)), Character.MIN_VALUE)); + return Optional.of(new StructureCompletion(WorkflowSyntax.eventActivityTypesFor(path.get(1), provider), Character.MIN_VALUE)); } if ("type".equals(currentKey) && (isChildOf(path, FIELD_ON, "workflow_dispatch", FIELD_INPUTS) @@ -401,7 +403,7 @@ private static Optional workflowValueCompletion(final Compl return Optional.of(new StructureCompletion(WorkflowSyntax.workflowInputTypesFor(path.get(1)), Character.MIN_VALUE)); } if (pathEndsWith(path, "permissions")) { - return Optional.of(new StructureCompletion(WorkflowSyntax.permissionValuesFor(currentKey), Character.MIN_VALUE)); + return Optional.of(new StructureCompletion(WorkflowSyntax.permissionValuesFor(currentKey, provider), Character.MIN_VALUE)); } if ("required".equals(currentKey) || "continue-on-error".equals(currentKey) @@ -412,39 +414,49 @@ private static Optional workflowValueCompletion(final Compl return Optional.empty(); } - private static Optional workflowEventFilterValueCompletion(final CompletionPsi completionPsi, final WorkflowLocation.KeyContext context) { - return eventFilterContext(context) - .map(eventFilter -> eventFilterValueCompletions(completionPsi.position(), eventFilter.event(), eventFilter.filter())) + private static Optional workflowEventFilterValueCompletion( + final CompletionPsi completionPsi, + final WorkflowLocation.KeyContext context, + final WorkflowSyntax.Provider provider + ) { + return eventFilterContext(context, provider) + .map(eventFilter -> eventFilterValueCompletions(completionPsi.position(), eventFilter.event(), eventFilter.filter(), provider)) .filter(values -> !values.isEmpty()) .map(values -> new StructureCompletion(values, Character.MIN_VALUE)); } - private static Optional eventFilterContext(final WorkflowLocation.KeyContext context) { + private static Optional eventFilterContext(final WorkflowLocation.KeyContext context, final WorkflowSyntax.Provider provider) { final List path = context.path(); final Optional currentKey = currentLineKey(context.currentLine()); if (currentKey.isPresent() && pathMatches(path, FIELD_ON, "*")) { final String event = path.get(1); final String filter = currentKey.get(); - return WorkflowSyntax.eventFilterKeysFor(event).containsKey(filter) + return WorkflowSyntax.eventFilterKeysFor(event, provider).containsKey(filter) ? Optional.of(new EventFilterContext(event, filter)) : Optional.empty(); } if (pathMatches(path, FIELD_ON, "*", "*")) { final String event = path.get(1); final String filter = path.get(2); - return WorkflowSyntax.eventFilterKeysFor(event).containsKey(filter) + return WorkflowSyntax.eventFilterKeysFor(event, provider).containsKey(filter) ? Optional.of(new EventFilterContext(event, filter)) : Optional.empty(); } return Optional.empty(); } - private static Map eventFilterValueCompletions(final PsiElement position, final String event, final String filter) { + private static Map eventFilterValueCompletions( + final PsiElement position, + final String event, + final String filter, + final WorkflowSyntax.Provider provider + ) { return switch (filter) { - case "types" -> WorkflowSyntax.eventActivityTypesFor(event); + case "types" -> WorkflowSyntax.eventActivityTypesFor(event, provider); case "branches", "branches-ignore" -> localGitRefs(position, "heads", "completion.workflow.eventFilter.branches"); case "tags", "tags-ignore" -> localGitRefs(position, "tags", "completion.workflow.eventFilter.tags"); case "paths", "paths-ignore" -> localProjectPaths(position); + case "cron" -> WorkflowSyntax.cronValues(provider); default -> Collections.emptyMap(); }; } diff --git a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntax.java b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntax.java index e13cb28..fe95264 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntax.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntax.java @@ -59,7 +59,7 @@ public class WorkflowSyntax { new GitHubSchemaProvider("dependabot-2.0", "Dependabot", WorkflowYaml::isDependabotFile), new GitHubSchemaProvider("github-action", "GitHub Action", WorkflowYaml::isActionFile), new GitHubSchemaProvider("github-funding", "GitHub Funding", WorkflowYaml::isFoundingFile), - new GitHubSchemaProvider("github-workflow", "GitHub Workflow", WorkflowYaml::isWorkflowFile), + new GitHubSchemaProvider("github-workflow", "GitHub Workflow", WorkflowYaml::isGithubWorkflowFile), new GitHubSchemaProvider("github-discussion", "GitHub Discussion", WorkflowYaml::isDiscussionFile), new GitHubSchemaProvider("github-issue-forms", "GitHub Issue Forms", WorkflowYaml::isIssueForms), new GitHubSchemaProvider("github-issue-config", "GitHub Workflow Issue Template configuration", WorkflowYaml::isIssueConfigFile), @@ -75,22 +75,22 @@ public class WorkflowSyntax { rule( (path, completion) -> isChildOf(path, FIELD_ON, "workflow_dispatch", FIELD_INPUTS) || isChildOf(path, FIELD_ON, "workflow_call", FIELD_INPUTS), - ignored -> workflowInputPropertyKeys(), + (ignored, provider) -> workflowInputPropertyKeys(), "inspection.workflow.syntax.unknownTriggerKey" ), rule( (path, completion) -> isChildOf(path, FIELD_ON, "workflow_call", FIELD_OUTPUTS), - ignored -> workflowOutputPropertyKeys(), + (ignored, provider) -> workflowOutputPropertyKeys(), "inspection.workflow.syntax.unknownTriggerKey" ), rule( (path, completion) -> isChildOf(path, FIELD_ON, "workflow_call", FIELD_SECRETS), - ignored -> workflowSecretPropertyKeys(), + (ignored, provider) -> workflowSecretPropertyKeys(), "inspection.workflow.syntax.unknownTriggerKey" ), rule( (path, completion) -> pathMatches(path, FIELD_ON, "*"), - path -> eventFilterKeysFor(path.get(path.size() - 1)), + (path, provider) -> eventFilterKeysFor(path.get(path.size() - 1), provider), "inspection.workflow.syntax.unknownTriggerFilter" ), rule((path, completion) -> pathEndsWith(path, "permissions"), "permission", "inspection.workflow.syntax.unknownPermission"), @@ -120,6 +120,24 @@ public class WorkflowSyntax { private WorkflowSyntax() { } + public enum Provider { + GITHUB, + GITEA + } + + /** + * Detects the workflow syntax provider from the edited file path. + * + * @param element any PSI element inside the edited file + * @return {@link Provider#GITEA} for {@code .gitea/workflows}, otherwise {@link Provider#GITHUB} + */ + public static Provider providerFor(final PsiElement element) { + return WorkflowYaml.getWorkflowFile(element) + .filter(WorkflowYaml::isGiteaWorkflowFile) + .map(ignored -> Provider.GITEA) + .orElse(Provider.GITHUB); + } + public static class FileIcon extends IconProvider { // IconLoader automatically resolves /icons/gitea_dark.svg in dark themes. private static final Icon GITEA_ICON = IconLoader.getIcon("/icons/gitea.svg", FileIcon.class); @@ -133,16 +151,22 @@ public Icon getIcon(@NotNull final PsiElement element, final int flags) { .filter(PsiFile.class::isInstance) .map(PsiFile.class::cast) .map(PsiFile::getVirtualFile) - .flatMap(virtualFile -> SCHEMA_FILE_PROVIDERS.stream() - .filter(GitHubSchemaProvider.class::isInstance) - .map(GitHubSchemaProvider.class::cast) - .filter(schemaProvider -> schemaProvider.isAvailable(virtualFile)) - .map(schema -> iconFor(virtualFile)) - .findFirst() - ) + .flatMap(FileIcon::iconForKnownFile) .orElse(null); } + private static Optional iconForKnownFile(final VirtualFile virtualFile) { + if (WorkflowPsi.toPath(virtualFile).filter(WorkflowYaml::isWorkflowFile).isPresent()) { + return Optional.of(iconFor(virtualFile)); + } + return SCHEMA_FILE_PROVIDERS.stream() + .filter(GitHubSchemaProvider.class::isInstance) + .map(GitHubSchemaProvider.class::cast) + .filter(schemaProvider -> schemaProvider.isAvailable(virtualFile)) + .map(schema -> iconFor(virtualFile)) + .findFirst(); + } + private static Icon iconFor(final VirtualFile virtualFile) { return isGiteaWorkflowFile(virtualFile) ? GITEA_ICON : AllIcons.Vcs.Vendors.Github; } @@ -381,28 +405,61 @@ static Map eventFilterKeys() { } public static Map eventFilterKeysFor(final String event) { - final Map result = table("eventFilter." + event); + return eventFilterKeysFor(event, Provider.GITHUB); + } + + /** + * Returns trigger filter keys valid for the selected workflow provider. + * + * @param event trigger event name + * @param provider syntax provider inferred from the workflow file + * @return localized filter keys keyed by YAML key name + */ + public static Map eventFilterKeysFor(final String event, final Provider provider) { + final Map result = table("eventFilter." + event, provider); return result.isEmpty() ? eventFilterKeys() : result; } public static Optional> completionKeysForPath(final List path) { - return knownKeysForPath(path, true).map(KnownKeys::values); + return completionKeysForPath(path, Provider.GITHUB); + } + + /** + * Returns structure completion keys for a YAML path and provider. + * + * @param path YAML key path before the caret + * @param provider syntax provider inferred from the workflow file + * @return localized completion keys when this plugin owns the path + */ + public static Optional> completionKeysForPath(final List path, final Provider provider) { + return knownKeysForPath(path, true, provider).map(KnownKeys::values); } public static Optional descriptionForKey(final YAMLKeyValue keyValue) { return WorkflowLocation.from(keyValue) .filter(WorkflowLocation::workflowFile) - .flatMap(location -> knownKeysForPath(location.path(), true) + .flatMap(location -> knownKeysForPath(location.path(), true, providerFor(keyValue)) .map(KnownKeys::values) .map(values -> values.get(location.keyValue().getKeyText()))) .filter(value -> !value.isBlank()); } public static Optional validationKeysForPath(final List path) { - return knownKeysForPath(path, false); + return validationKeysForPath(path, Provider.GITHUB); + } + + /** + * Returns validation keys for a YAML path and provider. + * + * @param path YAML key path at the inspected element + * @param provider syntax provider inferred from the workflow file + * @return allowed keys and diagnostic message when this plugin owns the path + */ + public static Optional validationKeysForPath(final List path, final Provider provider) { + return knownKeysForPath(path, false, provider); } - private static Optional knownKeysForPath(final List path, final boolean completion) { + private static Optional knownKeysForPath(final List path, final boolean completion, final Provider provider) { if (pathEndsWith(path, FIELD_ON, "workflow_dispatch", FIELD_INPUTS) || pathEndsWith(path, FIELD_ON, "workflow_call", FIELD_INPUTS) || pathEndsWith(path, FIELD_ON, "workflow_call", FIELD_OUTPUTS) @@ -410,12 +467,23 @@ private static Optional knownKeysForPath(final List path, fin return Optional.empty(); } return KEY_RULES.stream() - .flatMap(rule -> rule.known(path, completion).stream()) + .flatMap(rule -> rule.known(path, completion, provider).stream()) .findFirst(); } public static Map eventActivityTypesFor(final String event) { - return table("activity." + event); + return eventActivityTypesFor(event, Provider.GITHUB); + } + + /** + * Returns documented activity types for a trigger event. + * + * @param event trigger event name + * @param provider syntax provider inferred from the workflow file + * @return localized activity values keyed by YAML value + */ + public static Map eventActivityTypesFor(final String event, final Provider provider) { + return table("activity." + event, provider); } static Map permissionScopes() { @@ -427,14 +495,35 @@ static Map permissionValues() { } public static Map permissionValuesFor(final String permission) { + return permissionValuesFor(permission, Provider.GITHUB); + } + + /** + * Returns permission access values for a permission scope and provider. + * + * @param permission permission scope name + * @param provider syntax provider inferred from the workflow file + * @return localized access values keyed by YAML value + */ + public static Map permissionValuesFor(final String permission, final Provider provider) { final Map result = table("permissionValue." + permission); - return result.isEmpty() ? permissionValues() : result; + return provider == Provider.GITEA || result.isEmpty() ? permissionValues() : result; } public static Map permissionShorthandValues() { return table("permissionShorthand"); } + /** + * Returns shorthand permission values for the selected workflow provider. + * + * @param provider syntax provider inferred from the workflow file + * @return localized shorthand permission values keyed by YAML value + */ + public static Map permissionShorthandValues(final Provider provider) { + return table("permissionShorthand", provider); + } + static Map jobKeys() { return table("job"); } @@ -527,6 +616,26 @@ public static Map runnerLabels() { return table("runner"); } + /** + * Returns runner label completions for the selected workflow provider. + * + * @param provider syntax provider inferred from the workflow file + * @return localized runner labels keyed by YAML value + */ + public static Map runnerLabels(final Provider provider) { + return table("runner", provider); + } + + /** + * Returns schedule cron value completions for the selected workflow provider. + * + * @param provider syntax provider inferred from the workflow file + * @return localized cron values keyed by YAML value + */ + public static Map cronValues(final Provider provider) { + return table("cron", provider); + } + private static Map table(final String group) { final Map keys = Tables.DATA.getOrDefault(group, Collections.emptyMap()); if (keys.isEmpty()) { @@ -537,6 +646,30 @@ private static Map table(final String group) { return Collections.unmodifiableMap(result); } + private static Map table(final String group, final Provider provider) { + if (provider != Provider.GITEA) { + return table(group); + } + final Map gitea = table(group + ".gitea"); + if (gitea.isEmpty()) { + return table(group); + } + if (replacesGithubTable(group)) { + return gitea; + } + final Map result = new LinkedHashMap<>(table(group)); + result.putAll(gitea); + return Collections.unmodifiableMap(result); + } + + private static boolean replacesGithubTable(final String group) { + return "permission".equals(group) + || "permissionShorthand".equals(group) + || group.startsWith("permissionValue.") + || "runner".equals(group) + || "cron".equals(group); + } + private static Map> loadTables() { final Map> result = new LinkedHashMap<>(); try (BufferedReader reader = syntaxReader()) { @@ -588,7 +721,7 @@ private Tables() { } private static SyntaxRule rule(final PathPredicate predicate, final String table, final String messageKey) { - return rule(predicate, ignored -> table(table), messageKey); + return rule(predicate, (ignored, provider) -> table(table, provider), messageKey); } private static SyntaxRule rule(final PathPredicate predicate, final ValueProvider values, final String messageKey) { @@ -602,13 +735,13 @@ private interface PathPredicate { @FunctionalInterface private interface ValueProvider { - Map values(List path); + Map values(List path, Provider provider); } private record SyntaxRule(PathPredicate predicate, ValueProvider values, String messageKey) { - Optional known(final List path, final boolean completion) { + Optional known(final List path, final boolean completion, final Provider provider) { return predicate.matches(path, completion) - ? Optional.of(new KnownKeys(values.values(path), messageKey)) + ? Optional.of(new KnownKeys(values.values(path, provider), messageKey)) : Optional.empty(); } } diff --git a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowYaml.java b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowYaml.java index 2062ef9..ca24ed9 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowYaml.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowYaml.java @@ -287,6 +287,28 @@ && isYamlFile(path) || path.getName(path.getNameCount() - 3).toString().equalsIgnoreCase(".gitea")); } + /** + * Checks whether a path is a Gitea Actions workflow file. + * + * @param path file path to inspect + * @return true for YAML files under {@code .gitea/workflows} + */ + public static boolean isGiteaWorkflowFile(final Path path) { + return isWorkflowFile(path) + && path.getName(path.getNameCount() - 3).toString().equalsIgnoreCase(".gitea"); + } + + /** + * Checks whether a path is a GitHub Actions workflow file. + * + * @param path file path to inspect + * @return true for YAML files under {@code .github/workflows} + */ + public static boolean isGithubWorkflowFile(final Path path) { + return isWorkflowFile(path) + && path.getName(path.getNameCount() - 3).toString().equalsIgnoreCase(".github"); + } + public static boolean isWorkflowTemplatePropertiesFile(final Path path) { return path.getNameCount() > 2 && path.getName(path.getNameCount() - 3).toString().equalsIgnoreCase(".github") diff --git a/src/main/resources/github-docs/workflow-syntax.tsv b/src/main/resources/github-docs/workflow-syntax.tsv index f5eaa2c..f8c8f61 100644 --- a/src/main/resources/github-docs/workflow-syntax.tsv +++ b/src/main/resources/github-docs/workflow-syntax.tsv @@ -202,6 +202,15 @@ permission pull-requests completion.workflow.permission.pull-requests permission security-events completion.workflow.permission.security-events permission statuses completion.workflow.permission.statuses permission vulnerability-alerts completion.workflow.permission.vulnerability-alerts +permission.gitea actions completion.workflow.permission.actions +permission.gitea contents completion.workflow.permission.contents +permission.gitea code completion.workflow.permission.contents +permission.gitea releases completion.workflow.event.release +permission.gitea issues completion.workflow.permission.issues +permission.gitea pull-requests completion.workflow.permission.pull-requests +permission.gitea wiki completion.workflow.event.gollum +permission.gitea projects completion.workflow.event.project +permission.gitea packages completion.workflow.permission.packages permissionValue read completion.workflow.permission.value.read permissionValue write completion.workflow.permission.value.write permissionValue none completion.workflow.permission.value.none @@ -214,6 +223,13 @@ permissionValue.vulnerability-alerts none completion.workflow.permission.value.n permissionShorthand read-all completion.workflow.permission.shorthand.read-all permissionShorthand write-all completion.workflow.permission.shorthand.write-all permissionShorthand {} completion.workflow.permission.shorthand.empty +permissionShorthand.gitea read-all completion.workflow.permission.shorthand.read-all +permissionShorthand.gitea write-all completion.workflow.permission.shorthand.write-all +cron.gitea @yearly completion.workflow.eventFilter.cron +cron.gitea @monthly completion.workflow.eventFilter.cron +cron.gitea @weekly completion.workflow.eventFilter.cron +cron.gitea @daily completion.workflow.eventFilter.cron +cron.gitea @hourly completion.workflow.eventFilter.cron job name completion.workflow.job.name job permissions completion.workflow.job.permissions job needs completion.workflow.job.needs diff --git a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowMetadataTest.java b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowMetadataTest.java index aaf45b1..eb24cb8 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowMetadataTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowMetadataTest.java @@ -162,6 +162,10 @@ public void detectsWorkflowFilesOnlyUnderGithubWorkflowsDirectory() { assertThat(WorkflowYaml.isWorkflowFile(Path.of("repo", ".github", "workflows", "build.yml"))).isTrue(); assertThat(WorkflowYaml.isWorkflowFile(Path.of("repo", ".github", "workflows", "build.yaml"))).isTrue(); assertThat(WorkflowYaml.isWorkflowFile(Path.of("repo", ".gitea", "workflows", "build.yml"))).isTrue(); + assertThat(WorkflowYaml.isGithubWorkflowFile(Path.of("repo", ".github", "workflows", "build.yml"))).isTrue(); + assertThat(WorkflowYaml.isGithubWorkflowFile(Path.of("repo", ".gitea", "workflows", "build.yml"))).isFalse(); + assertThat(WorkflowYaml.isGiteaWorkflowFile(Path.of("repo", ".gitea", "workflows", "build.yml"))).isTrue(); + assertThat(WorkflowYaml.isGiteaWorkflowFile(Path.of("repo", ".github", "workflows", "build.yml"))).isFalse(); assertThat(WorkflowYaml.isWorkflowFile(Path.of("repo", ".github", "not-workflows", "build.yml"))).isFalse(); assertThat(WorkflowYaml.isWorkflowFile(Path.of("repo", "workflows", "build.yml"))).isFalse(); } diff --git a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java index 7e29745..2daaaa8 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java @@ -806,6 +806,37 @@ public void testPermissionScopeCompletionShowsDescriptions() { .containsEntry("models", "GitHub Models"); } + public void testGiteaPermissionScopeCompletionUsesGiteaTokenScopes() { + assertThat(completeGiteaWorkflow(""" + name: Completion + on: workflow_dispatch + permissions: + + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo ok + """)).contains("actions", "contents", "code", "releases", "wiki", "projects", "packages") + .doesNotContain("id-token", "statuses", "checks", "deployments", "pages", "security-events"); + } + + public void testGiteaPermissionScopeCompletionKeepsDescriptionsTranslated() { + assertThat(completeGiteaWorkflowTypeTexts(""" + name: Completion + on: workflow_dispatch + permissions: + + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo ok + """)).containsEntry("contents", "Repository contents") + .containsEntry("wiki", "Wiki page changed") + .containsEntry("projects", "Classic project changed"); + } + public void testPermissionValueCompletionSuggestsReadWriteNone() { assertThat(completeWorkflow(""" name: Completion @@ -833,6 +864,34 @@ public void testPermissionShorthandCompletionSuggestsReadAllWriteAllAndEmpty() { """)).contains("read-all", "write-all", "{}"); } + public void testGiteaPermissionShorthandCompletionUsesDocumentedValues() { + assertThat(completeGiteaWorkflow(""" + name: Completion + on: workflow_dispatch + permissions: + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo ok + """)).contains("read-all", "write-all") + .doesNotContain("{}"); + } + + public void testGiteaScheduleCompletionSuggestsCronAliases() { + assertThat(completeGiteaWorkflow(""" + name: Completion + on: + schedule: + - cron: + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo ok + """)).contains("@yearly", "@monthly", "@weekly", "@daily", "@hourly"); + } + public void testIdTokenPermissionCompletionSuggestsOnlyWriteOrNone() { assertThat(completeWorkflow(""" name: Completion @@ -2012,6 +2071,18 @@ private Map completeWorkflowTypeTexts(final String text) { )); } + private Map completeGiteaWorkflowTypeTexts(final String text) { + configureGiteaWorkflowProjectFile(text); + final LookupElement[] elements = myFixture.completeBasic(); + assertThat(elements).isNotNull(); + return java.util.Arrays.stream(elements) + .collect(Collectors.toMap( + LookupElement::getLookupString, + WorkflowSyntaxCompletionTest::typeText, + (left, right) -> left + )); + } + private void writeProjectFile(final String relativePath, final String content) throws Exception { final Path path = Path.of(getProject().getBasePath(), relativePath); Files.createDirectories(path.getParent()); diff --git a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java index 5c34d30..12d4bcf 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java @@ -311,6 +311,39 @@ public void testRestrictedPermissionValueIsHighlighted() { """); } + public void testGiteaSpecificPermissionScopesAreAccepted() { + assertGiteaWorkflowHighlights(""" + name: Syntax + on: workflow_dispatch + permissions: + code: read + releases: write + wiki: none + projects: read + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo ok + """); + } + + public void testGiteaRejectsGithubOnlyPermissionScopes() { + assertGiteaWorkflowHighlights(""" + name: Syntax + on: workflow_dispatch + permissions: + id-token: write + statuses: read + pages: write + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo ok + """); + } + public void testResolvedActionInputIsAccepted() { seedRemoteAction("owner/tool@v1", Map.of("known-input", "Known input"), Map.of()); diff --git a/src/test/java/com/github/yunabraska/githubworkflow/test/EditorFeatureTestCase.java b/src/test/java/com/github/yunabraska/githubworkflow/test/EditorFeatureTestCase.java index d243a65..f98fd80 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/test/EditorFeatureTestCase.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/test/EditorFeatureTestCase.java @@ -78,15 +78,33 @@ protected final void assertWorkflowHighlights(final String text) { myFixture.checkHighlighting(true, false, true); } + protected final void assertGiteaWorkflowHighlights(final String text) { + configureGiteaWorkflowProjectFile(text); + myFixture.checkHighlighting(true, false, true); + } + protected final void configureWorkflow(final String text) { myFixture.configureByText(YAMLFileType.YML, text); } protected final void configureWorkflowProjectFile(final String text) { + configureProjectFile(".github/workflows/workflow.yml", text); + } + + protected final void configureGiteaWorkflowProjectFile(final String text) { + configureProjectFile(".gitea/workflows/workflow.yml", text); + } + + protected final List completeGiteaWorkflow(final String text) { + configureGiteaWorkflowProjectFile(text); + return completeBasicLookupStrings(); + } + + private void configureProjectFile(final String path, final String text) { final int caretOffset = text.indexOf(""); final String fileText = text.replace("", ""); - myFixture.addFileToProject(".github/workflows/workflow.yml", fileText); - myFixture.configureFromTempProjectFile(".github/workflows/workflow.yml"); + myFixture.addFileToProject(path, fileText); + myFixture.configureFromTempProjectFile(path); if (caretOffset >= 0) { myFixture.getEditor().getCaretModel().moveToOffset(caretOffset); } From c3a23562dfc99aba6b931e68187f5e734abc0c7f Mon Sep 17 00:00:00 2001 From: Yuna Morgenstern Date: Thu, 11 Jun 2026 17:50:00 +0200 Subject: [PATCH 3/8] Harden Gitea workflow compatibility --- CHANGELOG.md | 2 + .../gitea-github-actions-compatibility.md | 13 +-- .../entry/WorkflowAnnotator.java | 83 ++++++++++++++++++- .../entry/WorkflowCompletion.java | 9 +- .../githubworkflow/syntax/Envs.java | 8 +- .../githubworkflow/syntax/Secrets.java | 7 +- .../syntax/WorkflowContextCatalog.java | 62 ++++++++++++++ .../resources/github-docs/workflow-syntax.tsv | 3 + .../messages/GitHubWorkflowBundle.properties | 6 ++ .../GitHubWorkflowBundle_ar.properties | 6 ++ .../GitHubWorkflowBundle_cs.properties | 6 ++ .../GitHubWorkflowBundle_de.properties | 6 ++ .../GitHubWorkflowBundle_es.properties | 6 ++ .../GitHubWorkflowBundle_fr.properties | 6 ++ .../GitHubWorkflowBundle_hi.properties | 6 ++ .../GitHubWorkflowBundle_id.properties | 6 ++ .../GitHubWorkflowBundle_it.properties | 6 ++ .../GitHubWorkflowBundle_ja.properties | 6 ++ .../GitHubWorkflowBundle_ko.properties | 6 ++ .../GitHubWorkflowBundle_nl.properties | 6 ++ .../GitHubWorkflowBundle_pl.properties | 6 ++ .../GitHubWorkflowBundle_pt_BR.properties | 6 ++ .../GitHubWorkflowBundle_ru.properties | 6 ++ .../GitHubWorkflowBundle_sv.properties | 6 ++ .../GitHubWorkflowBundle_th.properties | 6 ++ .../GitHubWorkflowBundle_tr.properties | 6 ++ .../GitHubWorkflowBundle_uk.properties | 6 ++ .../GitHubWorkflowBundle_vi.properties | 6 ++ .../GitHubWorkflowBundle_zh_CN.properties | 6 ++ .../git/GiteaDockerIntegrationTest.java | 34 +++++--- .../i18n/WorkflowMessagesTest.java | 8 +- .../syntax/WorkflowSyntaxCompletionTest.java | 50 +++++++++++ .../syntax/WorkflowValidationTest.java | 37 +++++++++ 33 files changed, 411 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a15999d..11c04e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ - `.gitea/workflows/*` files now get their own light/dark Gitea-flavored file icon instead of cosplaying GitHub. - Gitea workflow runs now use `GITEA_TOKEN`-style auth, clean browser links, and repo-level run discovery for `/api/v1`. - `.gitea/workflows/*` syntax now uses Gitea permission scopes and cron aliases instead of GitHub-only scope clutter. +- `.gitea/workflows/*` completion and validation now know `GITEA_TOKEN`, Gitea runner env vars, single-label `runs-on`, + documented expression functions, and job keys Gitea accepts but ignores. ## [2026.5.29] - 2026-05-29 diff --git a/doc/spec/gitea-github-actions-compatibility.md b/doc/spec/gitea-github-actions-compatibility.md index edd0770..bf099e4 100644 --- a/doc/spec/gitea-github-actions-compatibility.md +++ b/doc/spec/gitea-github-actions-compatibility.md @@ -1,6 +1,6 @@ # Gitea And GitHub Actions Compatibility -Last checked: 2026-06-10. +Last checked: 2026-06-11. This note tracks behavior where Gitea Actions is close to GitHub Actions, but not identical. The plugin should prefer shared behavior first and branch only where Gitea really differs. Tiny forks, not a hydra. @@ -35,17 +35,18 @@ shared behavior first and branch only where Gitea really differs. Tiny forks, no | Cron aliases | GitHub uses POSIX cron expressions | Gitea also accepts `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly` | `.gitea/workflows` cron completion suggests the Gitea aliases. | | Permission scopes | GitHub has GitHub-only scopes such as `id-token`, `statuses`, `pages` | Gitea has `code`, `releases`, `wiki`, `projects`, and a smaller shared scope set | `.gitea/workflows` completion and validation use the Gitea scope set. | | Permission shorthand | GitHub completion includes `read-all`, `write-all`, and `{}` | Gitea documents `read-all` and `write-all` scalar values | `.gitea/workflows` shorthand completion stays on documented Gitea values. | +| Complex `runs-on` | GitHub accepts richer runner targeting | Gitea supports scalar or single-item array forms | `.gitea/workflows` warns on multi-label arrays without affecting GitHub files. | +| Expression functions | GitHub has a wider function set | Gitea documents `always()` only | `.gitea/workflows` completion offers `always()` and warns on known GitHub-only functions. | +| Ignored job keys | GitHub executes `timeout-minutes`, `continue-on-error`, and `environment` | Gitea accepts but ignores them | `.gitea/workflows` keeps the keys valid and labels them as accepted runtime no-ops. | +| Runtime names | GitHub exposes `GITHUB_TOKEN` and `GITHUB_*` names | Gitea exposes `GITEA_TOKEN` and `GITEA_*` names too | `.gitea/workflows` completion prefers `secrets.GITEA_TOKEN` and includes Gitea runner env variables. | -## Differences To Keep Watching +## Known Limitations / Do Not Guess | Area | Gitea behavior | Plugin risk | Suggested handling | | --- | --- | --- | --- | -| Ignored job keys | Gitea currently ignores `jobs..timeout-minutes`, `continue-on-error`, and `environment`. | Warnings should not imply Gitea will enforce these keys. | Keep syntax valid, optionally show Gitea-specific weak info later. | -| Complex `runs-on` | Gitea supports scalar or single-item array forms, not GitHub's richer runner targeting. | Completion may suggest shapes Gitea does not execute. | Keep GitHub completion by default; add Gitea-aware validation only for `.gitea/workflows`. | -| Expressions | Gitea comparison says only `always()` is supported from expression functions. | GitHub-rich expression completion can overpromise in Gitea files. | Treat as future Gitea-specific inspection, not a parser fork. | | Problem matchers and annotations | Gitea ignores problem matchers and workflow command annotations. | Log rendering can still color warnings/errors locally; remote UI may not. | No code change needed unless we add Gitea-specific docs. | | Default action source | Gitea may resolve unqualified `uses:` through instance config (`github` or `self`). | Plugin cannot know server admin config. | Keep configured-server absolute URL support; do not guess admin config. | -| Secret and variable names | Gitea disallows user-created names starting with `GITHUB_` or `GITEA_`; variables are uppercased. | Future settings/UI for external variables must enforce Gitea naming rules. | Documented only; no external variable CRUD exists. | +| Secret and variable names | Gitea disallows user-created names starting with `GITHUB_` or `GITEA_`; variables are uppercased. | Future settings/UI for external variables must enforce Gitea naming rules. | Built-in names are completed; external variable CRUD does not exist yet. | ## Test Shape diff --git a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java index 07b8e60..215b842 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java @@ -25,6 +25,8 @@ import com.intellij.psi.impl.source.tree.LeafPsiElement; import org.jetbrains.annotations.NotNull; import org.jetbrains.yaml.psi.YAMLKeyValue; +import org.jetbrains.yaml.psi.YAMLSequence; +import org.jetbrains.yaml.psi.YAMLSequenceItem; import java.util.ArrayList; import java.util.Collection; @@ -32,8 +34,11 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -94,12 +99,16 @@ public class WorkflowAnnotator implements Annotator { "GITHUB_WORKFLOW_SCALAR_LITERAL", DefaultLanguageHighlighterColors.NUMBER ); + private static final Pattern EXPRESSION_FUNCTION = Pattern.compile("\\b([A-Za-z_][A-Za-z0-9_]*)\\s*\\("); + private static final Set GITHUB_EXPRESSION_FUNCTIONS = Set.copyOf(githubExpressionFunctionNames()); + private static final Set GITEA_EXPRESSION_FUNCTIONS = Set.of("always"); @Override public void annotate(@NotNull final PsiElement psiElement, @NotNull final AnnotationHolder holder) { annotationTrigger(holder, psiElement).ifPresent(trigger -> trigger .then(WorkflowAnnotator::processPsiElement) .then(WorkflowAnnotator::variableElementHandler) + .then(WorkflowAnnotator::validateGiteaExpressionFunctions) .then(WorkflowAnnotator::highlightVariableReferences) .then(WorkflowAnnotator::highlightDeclarations) .then(WorkflowAnnotator::highlightRunOutputs) @@ -155,7 +164,7 @@ private static void highlightRunnerVariables(final AnnotationHolder holder, fina .filter(LeafPsiElement.class::isInstance) .map(LeafPsiElement.class::cast) .filter(element -> getParent(element, FIELD_RUN).isPresent()) - .ifPresent(element -> DEFAULT_VALUE_MAP.get(FIELD_ENVS).get().keySet().forEach(name -> highlightWord(holder, element, name, RUNNER_VARIABLE))); + .ifPresent(element -> defaultEnvs(WorkflowSyntax.providerFor(element)).keySet().forEach(name -> highlightWord(holder, element, name, RUNNER_VARIABLE))); } private static void highlightWord( @@ -245,6 +254,26 @@ private static void validateWorkflowPropertyValue( if (pathEndsWith(path, "permissions")) { validateKnownValue(holder, element, WorkflowSyntax.permissionValuesFor(key, provider), "inspection.workflow.syntax.unknownPermissionValue"); } + if (provider == WorkflowSyntax.Provider.GITEA && pathMatches(path, FIELD_JOBS, "*") && "runs-on".equals(key)) { + validateGiteaRunsOn(holder, element); + } + } + + private static void validateGiteaRunsOn(final AnnotationHolder holder, final YAMLKeyValue element) { + if (!(element.getValue() instanceof YAMLSequence sequence)) { + return; + } + final List items = WorkflowPsi.getChildren(sequence, YAMLSequenceItem.class); + if (items.size() <= 1) { + return; + } + new SyntaxAnnotation( + GitHubWorkflowBundle.message("inspection.workflow.syntax.giteaRunsOnSingleLabel"), + null, + HighlightSeverity.WEAK_WARNING, + ProblemHighlightType.WEAK_WARNING, + null + ).createAnnotation(element, sequence.getTextRange(), holder); } private static void validateWorkflowInputPropertyValue( @@ -413,6 +442,58 @@ private static void variableElementHandler(final AnnotationHolder holder, final ); } + private static void validateGiteaExpressionFunctions(final AnnotationHolder holder, final PsiElement psiElement) { + Optional.of(psiElement) + .filter(LeafPsiElement.class::isInstance) + .map(LeafPsiElement.class::cast) + .filter(element -> WorkflowSyntax.providerFor(element) == WorkflowSyntax.Provider.GITEA) + .filter(isElementWithVariables(getParent(psiElement, FIELD_IF).orElse(null))) + .ifPresent(element -> expressionRanges(element).forEach(range -> validateGiteaExpressionRange(holder, element, range))); + } + + private static List expressionRanges(final LeafPsiElement element) { + final String text = element.getText(); + if (getParent(element, FIELD_IF).isPresent()) { + return text.isBlank() ? List.of() : List.of(new TextRange(0, text.length())); + } + final List result = new ArrayList<>(); + int index = 0; + while (index < text.length()) { + final int start = text.indexOf("${{", index); + if (start < 0) { + break; + } + final int bodyStart = start + 3; + final int end = text.indexOf("}}", bodyStart); + if (end < 0) { + break; + } + result.add(new TextRange(bodyStart, end)); + index = end + 2; + } + return result; + } + + private static void validateGiteaExpressionRange(final AnnotationHolder holder, final LeafPsiElement element, final TextRange relativeRange) { + final String body = element.getText().substring(relativeRange.getStartOffset(), relativeRange.getEndOffset()); + final Matcher matcher = EXPRESSION_FUNCTION.matcher(body); + while (matcher.find()) { + final String name = matcher.group(1); + if (!GITHUB_EXPRESSION_FUNCTIONS.contains(name) || GITEA_EXPRESSION_FUNCTIONS.contains(name)) { + continue; + } + final int start = element.getTextRange().getStartOffset() + relativeRange.getStartOffset() + matcher.start(1); + final int end = element.getTextRange().getStartOffset() + relativeRange.getStartOffset() + matcher.end(1); + new SyntaxAnnotation( + GitHubWorkflowBundle.message("inspection.workflow.syntax.giteaExpressionFunction", name + "()"), + null, + HighlightSeverity.WEAK_WARNING, + ProblemHighlightType.WEAK_WARNING, + null + ).createAnnotation(element, new TextRange(start, end), holder); + } + } + private static void highlightContext( final AnnotationHolder holder, final LeafPsiElement element, diff --git a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java index 1a79b70..981886c 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java @@ -55,7 +55,6 @@ import java.util.Map; import java.util.Optional; import java.nio.file.Path; -import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -185,12 +184,9 @@ private static boolean completeRunEnvironment(final CompletionTrigger trigger) { if (!isCompletingRunEnvironmentVariable(trigger.completionPsi())) { return false; } - final Map defaults = ofNullable(DEFAULT_VALUE_MAP.get(FIELD_ENVS)) - .map(Supplier::get) - .orElseGet(Collections::emptyMap); addLookupElements( trigger.resultSet().withPrefixMatcher(trigger.prefix()), - defaults, + defaultEnvs(WorkflowSyntax.providerFor(trigger.position())), NodeIcon.ICON_ENV, Character.MIN_VALUE ); @@ -863,8 +859,7 @@ private static List codeCompletionContext(final String field, fin } private static void addDefaultWorkflowCompletionItems(final int i, final PsiElement position, final Map> completionItemMap) { - ofNullable(DEFAULT_VALUE_MAP.getOrDefault(FIELD_DEFAULT, null)) - .map(Supplier::get) + ofNullable(expressionRoots(WorkflowSyntax.providerFor(position))) .map(map -> { final Map copyMap = new HashMap<>(map); Optional.of(listInputs(position)).filter(List::isEmpty).ifPresent(empty -> copyMap.remove(FIELD_INPUTS)); diff --git a/src/main/java/com/github/yunabraska/githubworkflow/syntax/Envs.java b/src/main/java/com/github/yunabraska/githubworkflow/syntax/Envs.java index 690b7da..d1327a5 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/syntax/Envs.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/syntax/Envs.java @@ -15,9 +15,9 @@ import java.util.function.Function; import java.util.stream.Collectors; -import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.DEFAULT_VALUE_MAP; import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.FIELD_ENVS; import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.FIELD_RUN; +import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.defaultEnvs; import static com.github.yunabraska.githubworkflow.syntax.WorkflowAnnotations.ifEnoughItems; import static com.github.yunabraska.githubworkflow.syntax.WorkflowAnnotations.isDefinedItem0; import static com.github.yunabraska.githubworkflow.syntax.WorkflowPsi.getAllElements; @@ -54,7 +54,7 @@ public static List listEnvs(final PsiElement psiElement) { addWorkflowEnvs(psiElement, result); //DEFAULT ENVS - addDefaultEnvs(result); + addDefaultEnvs(psiElement, result); return result; } @@ -103,8 +103,8 @@ private static Function, Map> toMapWithKeyAnd .collect(Collectors.toMap(YAMLKeyValue::getKeyText, keyValue -> getText(keyValue).orElse(""), (existing, replacement) -> existing)); } - private static void addDefaultEnvs(final List result) { - result.addAll(completionItemsOf(DEFAULT_VALUE_MAP.get(FIELD_ENVS).get(), ICON_ENV)); + private static void addDefaultEnvs(final PsiElement psiElement, final List result) { + result.addAll(completionItemsOf(defaultEnvs(WorkflowSyntax.providerFor(psiElement)), ICON_ENV)); } private Envs() { diff --git a/src/main/java/com/github/yunabraska/githubworkflow/syntax/Secrets.java b/src/main/java/com/github/yunabraska/githubworkflow/syntax/Secrets.java index 361425c..5dc1f6b 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/syntax/Secrets.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/syntax/Secrets.java @@ -38,6 +38,7 @@ public class Secrets { private static final String GITHUB_TOKEN = "GITHUB_TOKEN"; + private static final String GITEA_TOKEN = "GITEA_TOKEN"; public static void highLightSecrets( final AnnotationHolder holder, @@ -86,7 +87,11 @@ public static List listSecrets(final PsiElement psiElement) { LinkedHashMap::new ))) .orElseGet(LinkedHashMap::new); - result.putIfAbsent(GITHUB_TOKEN, GitHubWorkflowBundle.message("completion.secret.githubToken")); + if (WorkflowSyntax.providerFor(psiElement) == WorkflowSyntax.Provider.GITEA) { + result.putIfAbsent(GITEA_TOKEN, GitHubWorkflowBundle.message("completion.secret.giteaToken")); + } else { + result.putIfAbsent(GITHUB_TOKEN, GitHubWorkflowBundle.message("completion.secret.githubToken")); + } return completionItemsOf(result, ICON_SECRET_WORKFLOW); } diff --git a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java index 5763958..899fd70 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java @@ -11,6 +11,7 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -54,6 +55,29 @@ public class WorkflowContextCatalog { public static final String FIELD_OUTCOME = "outcome"; public static final Map>> DEFAULT_VALUE_MAP = initProcessorMap(); public static final Map SHELLS = initShells(); + private static final List GITEA_ENV_NAMES = List.of( + "GITEA_ACTIONS", + "GITEA_ACTIONS_RUNNER_VERSION", + "GITEA_ENV", + "GITEA_OUTPUT", + "GITEA_PATH", + "GITEA_STATE", + "GITEA_STEP_SUMMARY" + ); + private static final List GITHUB_EXPRESSION_FUNCTIONS = List.of( + "contains()", + "startsWith()", + "endsWith()", + "format()", + "join()", + "toJSON()", + "fromJSON()", + "hashFiles()", + "success()", + "always()", + "cancelled()", + "failure()" + ); private static Map>> initProcessorMap() { final Map>> result = new LinkedHashMap<>(); @@ -67,6 +91,44 @@ private static Map>> initProcessorMap() { return result; } + /** + * Returns built-in runner environment variables for the selected workflow provider. + * + * @param provider workflow provider inferred from the edited file + * @return immutable map keyed by environment variable name with localized descriptions + */ + public static Map defaultEnvs(final WorkflowSyntax.Provider provider) { + final Map result = new LinkedHashMap<>(getGitHubEnvs()); + if (provider == WorkflowSyntax.Provider.GITEA) { + GITEA_ENV_NAMES.forEach(name -> result.putIfAbsent(name, message("completion.env.gitea"))); + } + return Collections.unmodifiableMap(result); + } + + /** + * Returns root expression contexts and functions for the selected workflow provider. + * + * @param provider workflow provider inferred from the edited file + * @return immutable map keyed by expression token with localized descriptions + */ + public static Map expressionRoots(final WorkflowSyntax.Provider provider) { + final Map result = new LinkedHashMap<>(getCaretBracketItems()); + final List functions = provider == WorkflowSyntax.Provider.GITEA ? List.of("always()") : GITHUB_EXPRESSION_FUNCTIONS; + functions.forEach(function -> result.put(function, message("completion.expressionFunction"))); + return Collections.unmodifiableMap(result); + } + + /** + * Returns GitHub expression function names without parentheses. + * + * @return immutable list of known GitHub expression function names + */ + public static List githubExpressionFunctionNames() { + return GITHUB_EXPRESSION_FUNCTIONS.stream() + .map(function -> function.substring(0, function.indexOf('('))) + .toList(); + } + private static Map initShells() { final Map result = new LinkedHashMap<>(); result.put("bash", message("completion.shell.bash")); diff --git a/src/main/resources/github-docs/workflow-syntax.tsv b/src/main/resources/github-docs/workflow-syntax.tsv index f8c8f61..1e7fdd5 100644 --- a/src/main/resources/github-docs/workflow-syntax.tsv +++ b/src/main/resources/github-docs/workflow-syntax.tsv @@ -250,6 +250,9 @@ job services completion.workflow.job.services job uses completion.workflow.job.uses job with completion.workflow.job.with job secrets completion.workflow.job.secrets +job.gitea environment completion.workflow.job.gitea.ignored +job.gitea timeout-minutes completion.workflow.job.gitea.ignored +job.gitea continue-on-error completion.workflow.job.gitea.ignored defaultsRun shell completion.workflow.defaultsRun.shell defaultsRun working-directory completion.workflow.defaultsRun.working-directory concurrency group completion.workflow.concurrency.group diff --git a/src/main/resources/messages/GitHubWorkflowBundle.properties b/src/main/resources/messages/GitHubWorkflowBundle.properties index 7de3c27..66f0df2 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Unknown permission [{0}] inspection.workflow.syntax.unknownPermissionValue=Unknown permission value [{0}] inspection.workflow.syntax.unknownJobKey=Unknown job key [{0}] inspection.workflow.syntax.unknownStepKey=Unknown step key [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea supports one runner label here +inspection.workflow.syntax.giteaExpressionFunction=Gitea only documents always() here: [{0}] inspection.action.reload=Reload [{0}] inspection.action.unresolved=Unresolved [{0}] - check GitHub account access, private repository permissions, rate limits, missing refs, or missing action/workflow metadata inspection.action.jump=Jump to file [{0}] @@ -215,6 +217,9 @@ completion.context.github=Workflow run and event information from the GitHub con completion.context.gitea=Gitea-compatible alias for the GitHub Actions context. completion.context.runner=Information about the runner executing the current job. completion.secret.githubToken=Automatically created token for each workflow run. +completion.secret.giteaToken=Automatically created Gitea token for each workflow run. +completion.env.gitea=Gitea runner environment variable. +completion.expressionFunction=Expression function. Tiny logic spell. completion.remote.repository=Remote repository completion.uses.local.workflow=Local reusable workflow completion.uses.local.action=Local action @@ -317,6 +322,7 @@ completion.workflow.job.services=Sidecar service containers completion.workflow.job.uses=Reusable workflow to call completion.workflow.job.with=Inputs for called workflow completion.workflow.job.secrets=Secrets for called workflow +completion.workflow.job.gitea.ignored=Accepted by Gitea, ignored at runtime. completion.workflow.defaultsRun.shell=Default shell for run steps completion.workflow.defaultsRun.working-directory=Default working directory completion.workflow.concurrency.group=Lock name for queued runs diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ar.properties b/src/main/resources/messages/GitHubWorkflowBundle_ar.properties index b31bd60..5ea92c0 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ar.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ar.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=إذن غير معروف [{0}] inspection.workflow.syntax.unknownPermissionValue=قيمة إذن غير معروفة [{0}] inspection.workflow.syntax.unknownJobKey=مفتاح مهمة غير معروف [{0}] inspection.workflow.syntax.unknownStepKey=مفتاح خطوة غير معروف [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=يدعم Gitea هنا تسمية runner واحدة فقط +inspection.workflow.syntax.giteaExpressionFunction=يوثق Gitea هنا always() فقط: [{0}] inspection.action.reload=إعادة تحميل [{0}] inspection.action.unresolved=لم يتم حل [{0}] - تحقق من الوصول إلى حساب GitHub، أو أذونات المستودع الخاص، أو حدود المعدل، أو المراجع المفقودة، أو بيانات تعريف الإجراء/سير العمل المفقودة inspection.action.jump=انتقل إلى الملف [{0}] @@ -215,6 +217,9 @@ completion.context.github=تشغيل سير العمل ومعلومات الحد completion.context.gitea=الاسم المستعار المتوافق مع Gitea لسياق إجراءات GitHub. completion.context.runner=معلومات حول العداء الذي ينفذ الوظيفة الحالية. completion.secret.githubToken=الرمز المميز الذي تم إنشاؤه تلقائيًا لكل تشغيل سير عمل. +completion.secret.giteaToken=رمز Gitea يُنشأ تلقائياً لكل تشغيل workflow. +completion.env.gitea=متغير بيئة runner في Gitea. +completion.expressionFunction=دالة تعبير. تعويذة منطق صغيرة. completion.remote.repository=المستودع البعيد completion.uses.local.workflow=سير العمل المحلي القابل لإعادة الاستخدام completion.uses.local.action=العمل المحلي @@ -317,6 +322,7 @@ completion.workflow.job.services=حاويات خدمة Sidecar completion.workflow.job.uses=سير العمل القابل لإعادة الاستخدام للاتصال completion.workflow.job.with=مدخلات لسير العمل يسمى completion.workflow.job.secrets=أسرار تسمى سير العمل +completion.workflow.job.gitea.ignored=يقبله Gitea ويتجاهله وقت التشغيل. completion.workflow.defaultsRun.shell=الغلاف الافتراضي لخطوات التشغيل completion.workflow.defaultsRun.working-directory=دليل العمل الافتراضي completion.workflow.concurrency.group=اسم القفل لعمليات التشغيل في قائمة الانتظار diff --git a/src/main/resources/messages/GitHubWorkflowBundle_cs.properties b/src/main/resources/messages/GitHubWorkflowBundle_cs.properties index cf43d86..eab63e9 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_cs.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_cs.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Neznámé oprávnění [{0}] inspection.workflow.syntax.unknownPermissionValue=Neznámá hodnota oprávnění [{0}] inspection.workflow.syntax.unknownJobKey=Neznámý klíč úlohy [{0}] inspection.workflow.syntax.unknownStepKey=Neznámý krokový klíč [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea zde podporuje jeden štítek runneru +inspection.workflow.syntax.giteaExpressionFunction=Gitea zde dokumentuje jen always(): [{0}] inspection.action.reload=Znovu načíst [{0}] inspection.action.unresolved=Nevyřešeno [{0}] – zkontrolujte přístup k účtu GitHub, oprávnění soukromého úložiště, limity sazeb, chybějící odkazy nebo chybějící metadata akce/pracovního postupu inspection.action.jump=Přejít na soubor [{0}] @@ -215,6 +217,9 @@ completion.context.github=Informace o běhu pracovního postupu a událostech z completion.context.gitea=Alias kompatibilní s Gitea pro kontext akcí GitHub. completion.context.runner=Informace o běžci, který provádí aktuální úlohu. completion.secret.githubToken=Automaticky vytvořený token pro každé spuštění pracovního postupu. +completion.secret.giteaToken=Automaticky vytvořený token Gitea pro každý běh. +completion.env.gitea=Proměnná prostředí runneru Gitea. +completion.expressionFunction=Výrazová funkce. Malé kouzlo logiky. completion.remote.repository=Vzdálené úložiště completion.uses.local.workflow=Místní opakovaně použitelný pracovní postup completion.uses.local.action=Místní akce @@ -317,6 +322,7 @@ completion.workflow.job.services=Servisní kontejnery postranních vozíků completion.workflow.job.uses=Opakovaně použitelný pracovní postup pro volání completion.workflow.job.with=Vstupy pro volaný pracovní postup completion.workflow.job.secrets=Tajemství pro tzv. workflow +completion.workflow.job.gitea.ignored=Gitea přijme, za běhu ignoruje. completion.workflow.defaultsRun.shell=Výchozí shell pro kroky běhu completion.workflow.defaultsRun.working-directory=Výchozí pracovní adresář completion.workflow.concurrency.group=Název zámku pro běhy ve frontě diff --git a/src/main/resources/messages/GitHubWorkflowBundle_de.properties b/src/main/resources/messages/GitHubWorkflowBundle_de.properties index 681c372..5cc94cd 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_de.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_de.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Unbekannte Berechtigung [{0}] inspection.workflow.syntax.unknownPermissionValue=Unbekannter Berechtigungswert [{0}] inspection.workflow.syntax.unknownJobKey=Unbekannter Jobschlüssel [{0}] inspection.workflow.syntax.unknownStepKey=Unbekannter Schrittschlüssel [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea unterstützt hier nur ein Runner-Label +inspection.workflow.syntax.giteaExpressionFunction=Gitea dokumentiert hier nur always(): [{0}] inspection.action.reload=Neu laden [{0}] inspection.action.unresolved=Nicht gelöst [{0}] – Überprüfen Sie den GitHub-Kontozugriff, private Repository-Berechtigungen, Ratenbeschränkungen, fehlende Referenzen oder fehlende Aktions-/Workflow-Metadaten inspection.action.jump=Zur Datei springen [{0}] @@ -215,6 +217,9 @@ completion.context.github=Workflow-Ausführungs- und Ereignisinformationen aus d completion.context.gitea=Gitea-kompatibler Alias für den GitHub-Aktionskontext. completion.context.runner=Informationen über den Runner, der den aktuellen Job ausführt. completion.secret.githubToken=Automatisch erstelltes Token für jeden Workflow-Lauf. +completion.secret.giteaToken=Automatisch erstelltes Gitea-Token für jeden Workflow-Lauf. +completion.env.gitea=Gitea-Runner-Umgebungsvariable. +completion.expressionFunction=Ausdrucksfunktion. Kleine Logik-Magie. completion.remote.repository=Remote-Repository completion.uses.local.workflow=Lokaler wiederverwendbarer Workflow completion.uses.local.action=Lokale Aktion @@ -317,6 +322,7 @@ completion.workflow.job.services=Beiwagen-Servicecontainer completion.workflow.job.uses=Wiederverwendbarer Workflow zum Aufrufen completion.workflow.job.with=Eingaben für den aufgerufenen Workflow completion.workflow.job.secrets=Geheimnisse für den aufgerufenen Workflow +completion.workflow.job.gitea.ignored=Von Gitea akzeptiert, zur Laufzeit ignoriert. completion.workflow.defaultsRun.shell=Standard-Shell für Ausführungsschritte completion.workflow.defaultsRun.working-directory=Standardarbeitsverzeichnis completion.workflow.concurrency.group=Sperrname für Ausführungen in der Warteschlange diff --git a/src/main/resources/messages/GitHubWorkflowBundle_es.properties b/src/main/resources/messages/GitHubWorkflowBundle_es.properties index b3d79c8..525226d 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_es.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_es.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Permiso desconocido [{0}] inspection.workflow.syntax.unknownPermissionValue=Valor de permiso desconocido [{0}] inspection.workflow.syntax.unknownJobKey=Clave de trabajo desconocida [{0}] inspection.workflow.syntax.unknownStepKey=Tecla de paso desconocida [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea admite aquí una sola etiqueta de runner +inspection.workflow.syntax.giteaExpressionFunction=Gitea solo documenta always() aquí: [{0}] inspection.action.reload=Recargar [{0}] inspection.action.unresolved=Sin resolver [{0}]: verifique el acceso a la cuenta GitHub, los permisos del repositorio privado, los límites de velocidad, las referencias faltantes o los metadatos de acción/flujo de trabajo faltantes inspection.action.jump=Saltar al archivo [{0}] @@ -215,6 +217,9 @@ completion.context.github=Información de eventos y ejecución del flujo de trab completion.context.gitea=Alias compatible con Gitea para el contexto de acciones GitHub. completion.context.runner=Información sobre el corredor que ejecuta el trabajo actual. completion.secret.githubToken=Token creado automáticamente para cada ejecución de flujo de trabajo. +completion.secret.giteaToken=Token de Gitea creado automáticamente para cada ejecución. +completion.env.gitea=Variable de entorno del runner de Gitea. +completion.expressionFunction=Función de expresión. Hechizo lógico mini. completion.remote.repository=repositorio remoto completion.uses.local.workflow=Flujo de trabajo local reutilizable completion.uses.local.action=Acción local @@ -317,6 +322,7 @@ completion.workflow.job.services=Contenedores de servicio con sidecar completion.workflow.job.uses=Flujo de trabajo reutilizable para llamar completion.workflow.job.with=Entradas para el flujo de trabajo llamado completion.workflow.job.secrets=Secretos para el flujo de trabajo llamado +completion.workflow.job.gitea.ignored=Gitea lo acepta y lo ignora en ejecución. completion.workflow.defaultsRun.shell=Shell predeterminado para los pasos de ejecución completion.workflow.defaultsRun.working-directory=Directorio de trabajo predeterminado completion.workflow.concurrency.group=Bloquear nombre para ejecuciones en cola diff --git a/src/main/resources/messages/GitHubWorkflowBundle_fr.properties b/src/main/resources/messages/GitHubWorkflowBundle_fr.properties index 6b3d2c6..c8f382d 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_fr.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_fr.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Autorisation inconnue [{0}] inspection.workflow.syntax.unknownPermissionValue=Valeur d''autorisation inconnue [{0}] inspection.workflow.syntax.unknownJobKey=Clé de travail inconnue [{0}] inspection.workflow.syntax.unknownStepKey=Clé d''étape inconnue [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea accepte ici une seule étiquette de runner +inspection.workflow.syntax.giteaExpressionFunction=Gitea ne documente ici que always() : [{0}] inspection.action.reload=Recharger [{0}] inspection.action.unresolved=Non résolu [{0}] - vérifiez l''accès au compte GitHub, les autorisations du référentiel privé, les limites de débit, les références manquantes ou les métadonnées d''action/workflow manquantes inspection.action.jump=Aller au fichier [{0}] @@ -215,6 +217,9 @@ completion.context.github=Informations sur l''exécution et les événements du completion.context.gitea=Alias compatible Gitea pour le contexte d''actions GitHub. completion.context.runner=Informations sur le coureur exécutant la tâche en cours. completion.secret.githubToken=Jeton créé automatiquement pour chaque exécution de flux de travail. +completion.secret.giteaToken=Jeton Gitea créé automatiquement pour chaque exécution. +completion.env.gitea=Variable d’environnement du runner Gitea. +completion.expressionFunction=Fonction d’expression. Petit sort logique. completion.remote.repository=Dépôt distant completion.uses.local.workflow=Flux de travail local réutilisable completion.uses.local.action=Action locale @@ -317,6 +322,7 @@ completion.workflow.job.services=Conteneurs de service side-car completion.workflow.job.uses=Workflow réutilisable à appeler completion.workflow.job.with=Entrées pour le workflow appelé completion.workflow.job.secrets=Secrets du flux de travail appelé +completion.workflow.job.gitea.ignored=Accepté par Gitea, ignoré à l’exécution. completion.workflow.defaultsRun.shell=Shell par défaut pour les étapes d''exécution completion.workflow.defaultsRun.working-directory=Répertoire de travail par défaut completion.workflow.concurrency.group=Nom du verrou pour les exécutions en file d''attente diff --git a/src/main/resources/messages/GitHubWorkflowBundle_hi.properties b/src/main/resources/messages/GitHubWorkflowBundle_hi.properties index 65f6b80..71ab692 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_hi.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_hi.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=अज्ञात अनुमत inspection.workflow.syntax.unknownPermissionValue=अज्ञात अनुमति मान [{0}] inspection.workflow.syntax.unknownJobKey=अज्ञात कार्य कुंजी [{0}] inspection.workflow.syntax.unknownStepKey=अज्ञात चरण कुंजी [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea यहाँ एक ही runner लेबल समर्थन करता है +inspection.workflow.syntax.giteaExpressionFunction=Gitea यहाँ केवल always() दर्ज करता है: [{0}] inspection.action.reload=पुनः लोड करें [{0}] inspection.action.unresolved=अनसुलझा [{0}] - GitHub खाता पहुंच, निजी रिपॉजिटरी अनुमतियाँ, दर सीमा, गुम रेफरी, या गुम कार्रवाई/वर्कफ़्लो मेटाडेटा की जाँच करें inspection.action.jump=फ़ाइल पर जाएं [{0}] @@ -215,6 +217,9 @@ completion.context.github=GitHub संदर्भ से वर्कफ़् completion.context.gitea=GitHub क्रियाएँ संदर्भ के लिए Gitea-संगत उपनाम। completion.context.runner=वर्तमान कार्य निष्पादित करने वाले धावक के बारे में जानकारी। completion.secret.githubToken=प्रत्येक वर्कफ़्लो रन के लिए स्वचालित रूप से टोकन बनाया गया। +completion.secret.giteaToken=हर workflow रन के लिए अपने-आप बना Gitea टोकन। +completion.env.gitea=Gitea runner पर्यावरण चर। +completion.expressionFunction=अभिव्यक्ति फ़ंक्शन। छोटी लॉजिक जादूगरी। completion.remote.repository=दूरस्थ भंडार completion.uses.local.workflow=स्थानीय पुन: प्रयोज्य वर्कफ़्लो completion.uses.local.action=स्थानीय कार्रवाई @@ -317,6 +322,7 @@ completion.workflow.job.services=साइडकार सेवा कंटे completion.workflow.job.uses=कॉल करने के लिए पुन: प्रयोज्य वर्कफ़्लो completion.workflow.job.with=बुलाए गए वर्कफ़्लो के लिए इनपुट completion.workflow.job.secrets=बुलाए गए वर्कफ़्लो के लिए रहस्य +completion.workflow.job.gitea.ignored=Gitea स्वीकार करता है, runtime में अनदेखा करता है। completion.workflow.defaultsRun.shell=रन चरणों के लिए डिफ़ॉल्ट शेल completion.workflow.defaultsRun.working-directory=डिफ़ॉल्ट कार्यशील निर्देशिका completion.workflow.concurrency.group=कतारबद्ध रन के लिए लॉक नाम diff --git a/src/main/resources/messages/GitHubWorkflowBundle_id.properties b/src/main/resources/messages/GitHubWorkflowBundle_id.properties index 5115258..5d2a4c5 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_id.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_id.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Izin tidak diketahui [{0}] inspection.workflow.syntax.unknownPermissionValue=Nilai izin tidak diketahui [{0}] inspection.workflow.syntax.unknownJobKey=Kunci pekerjaan tidak diketahui [{0}] inspection.workflow.syntax.unknownStepKey=Kunci langkah tidak diketahui [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea mendukung satu label runner di sini +inspection.workflow.syntax.giteaExpressionFunction=Gitea hanya mendokumentasikan always() di sini: [{0}] inspection.action.reload=Muat ulang [{0}] inspection.action.unresolved=[{0}] yang belum terselesaikan - periksa akses akun GitHub, izin repositori pribadi, batas kapasitas, referensi yang hilang, atau metadata tindakan/alur kerja yang hilang inspection.action.jump=Lompat ke file [{0}] @@ -215,6 +217,9 @@ completion.context.github=Alur kerja dijalankan dan informasi peristiwa dari kon completion.context.gitea=Alias ​​yang kompatibel dengan Gitea untuk konteks Tindakan GitHub. completion.context.runner=Informasi tentang pelari yang menjalankan pekerjaan saat ini. completion.secret.githubToken=Token yang dibuat secara otomatis untuk setiap alur kerja yang dijalankan. +completion.secret.giteaToken=Token Gitea otomatis untuk setiap eksekusi workflow. +completion.env.gitea=Variabel lingkungan runner Gitea. +completion.expressionFunction=Fungsi ekspresi. Mantra logika mini. completion.remote.repository=Repositori jarak jauh completion.uses.local.workflow=Alur kerja lokal yang dapat digunakan kembali completion.uses.local.action=Tindakan lokal @@ -317,6 +322,7 @@ completion.workflow.job.services=Kontainer servis sespan completion.workflow.job.uses=Alur kerja yang dapat digunakan kembali untuk menelepon completion.workflow.job.with=Input untuk alur kerja yang disebut completion.workflow.job.secrets=Rahasia untuk alur kerja yang disebut +completion.workflow.job.gitea.ignored=Diterima Gitea, diabaikan saat runtime. completion.workflow.defaultsRun.shell=Shell default untuk langkah-langkah yang dijalankan completion.workflow.defaultsRun.working-directory=Direktori kerja default completion.workflow.concurrency.group=Kunci nama untuk proses yang antri diff --git a/src/main/resources/messages/GitHubWorkflowBundle_it.properties b/src/main/resources/messages/GitHubWorkflowBundle_it.properties index 8f26b11..2673e7f 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_it.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_it.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Autorizzazione sconosciuta [{0}] inspection.workflow.syntax.unknownPermissionValue=Valore dell''autorizzazione sconosciuto [{0}] inspection.workflow.syntax.unknownJobKey=Chiave lavoro sconosciuta [{0}] inspection.workflow.syntax.unknownStepKey=Tasto passaggio sconosciuto [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea supporta qui una sola etichetta runner +inspection.workflow.syntax.giteaExpressionFunction=Gitea documenta qui solo always(): [{0}] inspection.action.reload=Ricarica [{0}] inspection.action.unresolved=Non risolto [{0}]: controlla l''accesso all''account GitHub, le autorizzazioni del repository privato, i limiti di velocità, i riferimenti mancanti o i metadati di azioni/flussi di lavoro mancanti inspection.action.jump=Vai al file [{0}] @@ -215,6 +217,9 @@ completion.context.github=Informazioni sull''esecuzione e sugli eventi del fluss completion.context.gitea=Alias compatibile con Gitea per il contesto Azioni GitHub. completion.context.runner=Informazioni sul corridore che sta eseguendo il lavoro corrente. completion.secret.githubToken=Token creato automaticamente per ogni esecuzione del flusso di lavoro. +completion.secret.giteaToken=Token Gitea creato automaticamente per ogni esecuzione. +completion.env.gitea=Variabile d’ambiente del runner Gitea. +completion.expressionFunction=Funzione di espressione. Mini incantesimo logico. completion.remote.repository=Archivio remoto completion.uses.local.workflow=Flusso di lavoro riutilizzabile locale completion.uses.local.action=Azione locale @@ -317,6 +322,7 @@ completion.workflow.job.services=Contenitori di servizio sidecar completion.workflow.job.uses=Flusso di lavoro riutilizzabile per chiamare completion.workflow.job.with=Input per il flusso di lavoro chiamato completion.workflow.job.secrets=Segreti per il flusso di lavoro chiamato +completion.workflow.job.gitea.ignored=Accettato da Gitea, ignorato a runtime. completion.workflow.defaultsRun.shell=Shell predefinita per i passaggi di esecuzione completion.workflow.defaultsRun.working-directory=Directory di lavoro predefinita completion.workflow.concurrency.group=Blocca il nome per le esecuzioni in coda diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ja.properties b/src/main/resources/messages/GitHubWorkflowBundle_ja.properties index 0b7923f..2ad83d8 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ja.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ja.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=不明な権限 [{0}] inspection.workflow.syntax.unknownPermissionValue=不明な権限値 [{0}] inspection.workflow.syntax.unknownJobKey=不明なジョブキー [{0}] inspection.workflow.syntax.unknownStepKey=不明なステップキー [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea はここで runner ラベルを1つだけサポートします +inspection.workflow.syntax.giteaExpressionFunction=Gitea がここで文書化しているのは always() だけです: [{0}] inspection.action.reload=リロード [{0}] inspection.action.unresolved=未解決 [{0}] - GitHub アカウント アクセス、プライベート リポジトリのアクセス許可、レート制限、参照の欠落、またはアクション/ワークフローのメタデータの欠落を確認してください。 inspection.action.jump=ファイル[{0}]へジャンプ @@ -215,6 +217,9 @@ completion.context.github=GitHub コンテキストからのワークフロー completion.context.gitea=GitHub アクション コンテキストの Gitea 互換のエイリアス。 completion.context.runner=現在のジョブを実行しているランナーに関する情報。 completion.secret.githubToken=ワークフロー実行ごとに自動的に作成されるトークン。 +completion.secret.giteaToken=各ワークフロー実行で自動作成される Gitea トークン。 +completion.env.gitea=Gitea runner の環境変数。 +completion.expressionFunction=式関数。小さな論理呪文。 completion.remote.repository=リモートリポジトリ completion.uses.local.workflow=ローカルで再利用可能なワークフロー completion.uses.local.action=ローカルアクション @@ -317,6 +322,7 @@ completion.workflow.job.services=サイドカーサービスコンテナ completion.workflow.job.uses=呼び出すための再利用可能なワークフロー completion.workflow.job.with=呼び出されたワークフローの入力 completion.workflow.job.secrets=呼び出されたワークフローのシークレット +completion.workflow.job.gitea.ignored=Gitea は受け入れますが、実行時は無視します。 completion.workflow.defaultsRun.shell=実行ステップのデフォルトのシェル completion.workflow.defaultsRun.working-directory=デフォルトの作業ディレクトリ completion.workflow.concurrency.group=キューに入れられた実行のロック名 diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ko.properties b/src/main/resources/messages/GitHubWorkflowBundle_ko.properties index 3a0389b..dce0cff 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ko.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ko.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=알 수 없는 권한 [{0}] inspection.workflow.syntax.unknownPermissionValue=알 수 없는 권한 값 [{0}] inspection.workflow.syntax.unknownJobKey=알 수 없는 작업 키 [{0}] inspection.workflow.syntax.unknownStepKey=알 수 없는 단계 키 [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea는 여기서 runner 레이블 하나만 지원합니다 +inspection.workflow.syntax.giteaExpressionFunction=Gitea는 여기서 always()만 문서화합니다: [{0}] inspection.action.reload=[{0}] 새로고침 inspection.action.unresolved=해결되지 않음 [{0}] - GitHub 계정 액세스, 개인 저장소 권한, 속도 제한, 누락된 참조 또는 누락된 작업/워크플로 메타데이터를 확인하세요. inspection.action.jump=[{0}] 파일로 이동 @@ -215,6 +217,9 @@ completion.context.github=GitHub 컨텍스트의 워크플로 실행 및 이벤 completion.context.gitea=GitHub 작업 컨텍스트에 대한 Gitea 호환 별칭입니다. completion.context.runner=현재 작업을 실행하는 러너에 대한 정보입니다. completion.secret.githubToken=각 워크플로 실행에 대해 자동으로 생성된 토큰입니다. +completion.secret.giteaToken=각 워크플로 실행마다 자동 생성되는 Gitea 토큰입니다. +completion.env.gitea=Gitea runner 환경 변수입니다. +completion.expressionFunction=표현식 함수. 작은 논리 주문. completion.remote.repository=원격 저장소 completion.uses.local.workflow=로컬 재사용 가능한 워크플로우 completion.uses.local.action=지역 활동 @@ -317,6 +322,7 @@ completion.workflow.job.services=사이드카 서비스 컨테이너 completion.workflow.job.uses=호출에 재사용 가능한 워크플로 completion.workflow.job.with=호출된 워크플로에 대한 입력 completion.workflow.job.secrets=호출된 워크플로의 비밀 +completion.workflow.job.gitea.ignored=Gitea는 받아들이지만 실행 시 무시합니다. completion.workflow.defaultsRun.shell=실행 단계의 기본 셸 completion.workflow.defaultsRun.working-directory=기본 작업 디렉터리 completion.workflow.concurrency.group=대기 중인 실행에 대한 잠금 이름 diff --git a/src/main/resources/messages/GitHubWorkflowBundle_nl.properties b/src/main/resources/messages/GitHubWorkflowBundle_nl.properties index ece386f..4051755 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_nl.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_nl.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Onbekende toestemming [{0}] inspection.workflow.syntax.unknownPermissionValue=Onbekende machtigingswaarde [{0}] inspection.workflow.syntax.unknownJobKey=Onbekende taaksleutel [{0}] inspection.workflow.syntax.unknownStepKey=Onbekende stapsleutel [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea ondersteunt hier één runnerlabel +inspection.workflow.syntax.giteaExpressionFunction=Gitea documenteert hier alleen always(): [{0}] inspection.action.reload=Herladen [{0}] inspection.action.unresolved=Onopgelost [{0}] - controleer GitHub-accounttoegang, machtigingen voor privérepository, snelheidslimieten, ontbrekende referenties of ontbrekende metadata van acties/workflows inspection.action.jump=Ga naar bestand [{0}] @@ -215,6 +217,9 @@ completion.context.github=Workflowuitvoering en gebeurtenisinformatie uit de Git completion.context.gitea=Gitea-compatibele alias voor de GitHub Actions-context. completion.context.runner=Informatie over de hardloper die de huidige taak uitvoert. completion.secret.githubToken=Automatisch aangemaakt token voor elke workflowuitvoering. +completion.secret.giteaToken=Automatisch gemaakt Gitea-token voor elke workflowrun. +completion.env.gitea=Omgevingsvariabele van de Gitea-runner. +completion.expressionFunction=Expressiefunctie. Klein logicaspreukje. completion.remote.repository=Externe opslagplaats completion.uses.local.workflow=Lokale herbruikbare workflow completion.uses.local.action=Lokale actie @@ -317,6 +322,7 @@ completion.workflow.job.services=Zijspan servicecontainers completion.workflow.job.uses=Herbruikbare workflow om te bellen completion.workflow.job.with=Ingangen voor de opgeroepen workflow completion.workflow.job.secrets=Geheimen voor de opgeroepen workflow +completion.workflow.job.gitea.ignored=Geaccepteerd door Gitea, genegeerd tijdens runtime. completion.workflow.defaultsRun.shell=Standaardshell voor uitvoeringsstappen completion.workflow.defaultsRun.working-directory=Standaard werkmap completion.workflow.concurrency.group=Naam van vergrendeling voor uitvoeringen in de wachtrij diff --git a/src/main/resources/messages/GitHubWorkflowBundle_pl.properties b/src/main/resources/messages/GitHubWorkflowBundle_pl.properties index a3fbd4d..3e28c8f 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_pl.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_pl.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Nieznane uprawnienia [{0}] inspection.workflow.syntax.unknownPermissionValue=Nieznana wartość uprawnienia [{0}] inspection.workflow.syntax.unknownJobKey=Nieznany klucz zadania [{0}] inspection.workflow.syntax.unknownStepKey=Nieznany klucz kroku [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea obsługuje tu jedną etykietę runnera +inspection.workflow.syntax.giteaExpressionFunction=Gitea dokumentuje tu tylko always(): [{0}] inspection.action.reload=Załaduj ponownie [{0}] inspection.action.unresolved=Nierozwiązany [{0}] — sprawdź dostęp do konta GitHub, uprawnienia do prywatnego repozytorium, limity szybkości, brakujące referencje lub brakujące metadane akcji/przepływu pracy inspection.action.jump=Skocz do pliku [{0}] @@ -215,6 +217,9 @@ completion.context.github=Informacje o przebiegu przepływu pracy i zdarzeniach completion.context.gitea=Alias zgodny z Gitea dla kontekstu akcji GitHub. completion.context.runner=Informacja o biegaczu wykonującym bieżące zadanie. completion.secret.githubToken=Automatycznie tworzony token dla każdego uruchomienia przepływu pracy. +completion.secret.giteaToken=Automatycznie utworzony token Gitea dla każdego uruchomienia. +completion.env.gitea=Zmienna środowiskowa runnera Gitea. +completion.expressionFunction=Funkcja wyrażenia. Mały czar logiki. completion.remote.repository=Zdalne repozytorium completion.uses.local.workflow=Lokalny przepływ pracy wielokrotnego użytku completion.uses.local.action=Akcja lokalna @@ -317,6 +322,7 @@ completion.workflow.job.services=Kontenery serwisowe z wózkiem bocznym completion.workflow.job.uses=Przepływ pracy wielokrotnego użytku, do którego można zadzwonić completion.workflow.job.with=Dane wejściowe dla wywoływanego przepływu pracy completion.workflow.job.secrets=Sekrety wywoływanego przepływu pracy +completion.workflow.job.gitea.ignored=Akceptowane przez Gitea, ignorowane w runtime. completion.workflow.defaultsRun.shell=Domyślna powłoka dla kroków uruchamiania completion.workflow.defaultsRun.working-directory=Domyślny katalog roboczy completion.workflow.concurrency.group=Zablokuj nazwę dla uruchomień w kolejce diff --git a/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties b/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties index deaffc5..8403c4a 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Permissão desconhecida [{0}] inspection.workflow.syntax.unknownPermissionValue=Valor de permissão desconhecido [{0}] inspection.workflow.syntax.unknownJobKey=Chave de trabalho desconhecida [{0}] inspection.workflow.syntax.unknownStepKey=Chave de etapa desconhecida [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea aceita aqui só um rótulo de runner +inspection.workflow.syntax.giteaExpressionFunction=Gitea documenta aqui só always(): [{0}] inspection.action.reload=Recarregar [{0}] inspection.action.unresolved=Não resolvido [{0}] - verifique o acesso à conta GitHub, permissões de repositório privado, limites de taxa, referências ausentes ou metadados de ação/fluxo de trabalho ausentes inspection.action.jump=Ir para o arquivo [{0}] @@ -215,6 +217,9 @@ completion.context.github=Informações de evento e execução de fluxo de traba completion.context.gitea=Alias compatível com Gitea para o contexto de ações GitHub. completion.context.runner=Informações sobre o executor que está executando o trabalho atual. completion.secret.githubToken=Token criado automaticamente para cada execução de fluxo de trabalho. +completion.secret.giteaToken=Token Gitea criado automaticamente para cada execução. +completion.env.gitea=Variável de ambiente do runner Gitea. +completion.expressionFunction=Função de expressão. Feitiço lógico mini. completion.remote.repository=Repositório remoto completion.uses.local.workflow=Fluxo de trabalho reutilizável local completion.uses.local.action=Ação local @@ -317,6 +322,7 @@ completion.workflow.job.services=Contêineres de serviço sidecar completion.workflow.job.uses=Fluxo de trabalho reutilizável para ligar completion.workflow.job.with=Entradas para fluxo de trabalho chamado completion.workflow.job.secrets=Segredos para o fluxo de trabalho chamado +completion.workflow.job.gitea.ignored=Aceito pelo Gitea, ignorado em runtime. completion.workflow.defaultsRun.shell=Shell padrão para etapas de execução completion.workflow.defaultsRun.working-directory=Diretório de trabalho padrão completion.workflow.concurrency.group=Bloquear nome para execuções na fila diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ru.properties b/src/main/resources/messages/GitHubWorkflowBundle_ru.properties index 8e7eb95..fd3d737 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ru.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ru.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Неизвестное разреш inspection.workflow.syntax.unknownPermissionValue=Неизвестное значение разрешения [{0}] inspection.workflow.syntax.unknownJobKey=Неизвестный ключ задания [{0}] inspection.workflow.syntax.unknownStepKey=Неизвестная клавиша шага [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea поддерживает здесь одну метку раннера +inspection.workflow.syntax.giteaExpressionFunction=Gitea документирует здесь только always(): [{0}] inspection.action.reload=Перезагрузить [{0}] inspection.action.unresolved=Неразрешено [{0}] — проверьте доступ к учетной записи GitHub, разрешения частного репозитория, ограничения скорости, отсутствующие ссылки или отсутствующие метаданные действий/рабочих процессов. inspection.action.jump=Перейти к файлу [{0}] @@ -215,6 +217,9 @@ completion.context.github=Информация о запуске рабочег completion.context.gitea=Совместимый с Gitea псевдоним для контекста действий GitHub. completion.context.runner=Информация о бегуне, выполняющем текущее задание. completion.secret.githubToken=Автоматически создается токен для каждого запуска рабочего процесса. +completion.secret.giteaToken=Автоматический токен Gitea для каждого запуска workflow. +completion.env.gitea=Переменная окружения раннера Gitea. +completion.expressionFunction=Функция выражения. Маленькое заклинание логики. completion.remote.repository=Удаленный репозиторий completion.uses.local.workflow=Локальный многоразовый рабочий процесс completion.uses.local.action=Местное действие @@ -317,6 +322,7 @@ completion.workflow.job.services=Сервисные контейнеры с ко completion.workflow.job.uses=Многоразовый рабочий процесс для вызова completion.workflow.job.with=Входные данные для вызванного рабочего процесса completion.workflow.job.secrets=Секреты вызываемого рабочего процесса +completion.workflow.job.gitea.ignored=Gitea принимает, но игнорирует при выполнении. completion.workflow.defaultsRun.shell=Оболочка по умолчанию для шагов выполнения completion.workflow.defaultsRun.working-directory=Рабочий каталог по умолчанию completion.workflow.concurrency.group=Заблокировать имя для запусков в очереди diff --git a/src/main/resources/messages/GitHubWorkflowBundle_sv.properties b/src/main/resources/messages/GitHubWorkflowBundle_sv.properties index b78045b..a4987bf 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_sv.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_sv.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Okänd behörighet [{0}] inspection.workflow.syntax.unknownPermissionValue=Okänt behörighetsvärde [{0}] inspection.workflow.syntax.unknownJobKey=Okänd jobbnyckel [{0}] inspection.workflow.syntax.unknownStepKey=Okänd stegnyckel [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea stöder en runneretikett här +inspection.workflow.syntax.giteaExpressionFunction=Gitea dokumenterar bara always() här: [{0}] inspection.action.reload=Ladda om [{0}] inspection.action.unresolved=Olöst [{0}] - kontrollera GitHub-kontoåtkomst, behörigheter för privata arkiv, hastighetsgränser, saknade refs eller saknade åtgärds-/arbetsflödesmetadata inspection.action.jump=Hoppa till fil [{0}] @@ -215,6 +217,9 @@ completion.context.github=Arbetsflödeskörning och händelseinformation från G completion.context.gitea=Gitea-kompatibelt alias för GitHub Actions-kontexten. completion.context.runner=Information om löparen som utför det aktuella jobbet. completion.secret.githubToken=Automatiskt skapad token för varje arbetsflödeskörning. +completion.secret.giteaToken=Automatiskt skapat Gitea-token för varje körning. +completion.env.gitea=Miljövariabel för Gitea-runnern. +completion.expressionFunction=Uttrycksfunktion. Liten logikformel. completion.remote.repository=Fjärrförvar completion.uses.local.workflow=Lokalt återanvändbart arbetsflöde completion.uses.local.action=Lokal handling @@ -317,6 +322,7 @@ completion.workflow.job.services=Sidovagnsservicecontainrar completion.workflow.job.uses=Återanvändbart arbetsflöde att ringa completion.workflow.job.with=Ingångar för anropat arbetsflöde completion.workflow.job.secrets=Hemligheter för kallat arbetsflöde +completion.workflow.job.gitea.ignored=Accepteras av Gitea, ignoreras vid körning. completion.workflow.defaultsRun.shell=Standardskal för körsteg completion.workflow.defaultsRun.working-directory=Standard arbetskatalog completion.workflow.concurrency.group=Låsnamn för körningar i kö diff --git a/src/main/resources/messages/GitHubWorkflowBundle_th.properties b/src/main/resources/messages/GitHubWorkflowBundle_th.properties index f55aed7..725f495 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_th.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_th.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=สิทธิ์ที่ไม inspection.workflow.syntax.unknownPermissionValue=ค่าสิทธิ์ที่ไม่รู้จัก [{0}] inspection.workflow.syntax.unknownJobKey=รหัสงานที่ไม่รู้จัก [{0}] inspection.workflow.syntax.unknownStepKey=รหัสขั้นตอนที่ไม่รู้จัก [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea รองรับป้าย runner เดียวที่นี่ +inspection.workflow.syntax.giteaExpressionFunction=Gitea ระบุไว้ที่นี่เฉพาะ always(): [{0}] inspection.action.reload=โหลดซ้ำ [{0}] inspection.action.unresolved=ยังไม่ได้รับการแก้ไข [{0}] - ตรวจสอบการเข้าถึงบัญชี GitHub, สิทธิ์ของพื้นที่เก็บข้อมูลส่วนตัว, ขีดจำกัดอัตรา, ข้อมูลอ้างอิงหายไป หรือข้อมูลเมตาการดำเนินการ/เวิร์กโฟลว์หายไป inspection.action.jump=ข้ามไปที่ไฟล์ [{0}] @@ -215,6 +217,9 @@ completion.context.github=ข้อมูลการรันเวิร์ก completion.context.gitea=นามแฝงที่เข้ากันได้กับ Gitea สำหรับบริบทการดำเนินการ GitHub completion.context.runner=ข้อมูลเกี่ยวกับนักวิ่งที่ดำเนินงานปัจจุบัน completion.secret.githubToken=โทเค็นที่สร้างขึ้นโดยอัตโนมัติสำหรับการรันเวิร์กโฟลว์แต่ละครั้ง +completion.secret.giteaToken=โทเค็น Gitea ที่สร้างอัตโนมัติสำหรับทุก workflow run. +completion.env.gitea=ตัวแปรสภาพแวดล้อมของ Gitea runner. +completion.expressionFunction=ฟังก์ชันนิพจน์ เวทตรรกะจิ๋ว. completion.remote.repository=พื้นที่เก็บข้อมูลระยะไกล completion.uses.local.workflow=เวิร์กโฟลว์ที่ใช้ซ้ำได้ในท้องถิ่น completion.uses.local.action=การกระทำในท้องถิ่น @@ -317,6 +322,7 @@ completion.workflow.job.services=ตู้คอนเทนเนอร์บ completion.workflow.job.uses=เวิร์กโฟลว์ที่ใช้ซ้ำได้ในการโทร completion.workflow.job.with=อินพุตสำหรับเวิร์กโฟลว์ที่เรียกว่า completion.workflow.job.secrets=ข้อมูลลับสำหรับเวิร์กโฟลว์ที่เรียกว่า +completion.workflow.job.gitea.ignored=Gitea รับไว้ แต่ไม่ใช้ตอน runtime. completion.workflow.defaultsRun.shell=เชลล์ดีฟอลต์สำหรับขั้นตอนการรัน completion.workflow.defaultsRun.working-directory=ไดเร็กทอรีการทำงานเริ่มต้น completion.workflow.concurrency.group=ล็อคชื่อสำหรับการรันที่อยู่ในคิว diff --git a/src/main/resources/messages/GitHubWorkflowBundle_tr.properties b/src/main/resources/messages/GitHubWorkflowBundle_tr.properties index 82f7b9a..8a05287 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_tr.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_tr.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Bilinmeyen izin [{0}] inspection.workflow.syntax.unknownPermissionValue=Bilinmeyen izin değeri [{0}] inspection.workflow.syntax.unknownJobKey=Bilinmeyen iş anahtarı [{0}] inspection.workflow.syntax.unknownStepKey=Bilinmeyen adım anahtarı [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea burada tek runner etiketi destekler +inspection.workflow.syntax.giteaExpressionFunction=Gitea burada yalnız always() belgeler: [{0}] inspection.action.reload=[{0}]''i yeniden yükle inspection.action.unresolved=Çözümlenmemiş [{0}] - GitHub hesap erişimini, özel depo izinlerini, hız sınırlarını, eksik referansları veya eksik eylem/iş akışı meta verilerini kontrol edin inspection.action.jump=[{0}] dosyasına atla @@ -215,6 +217,9 @@ completion.context.github=GitHub bağlamından iş akışı çalıştırması ve completion.context.gitea=GitHub Eylemleri bağlamı için Gitea uyumlu takma ad. completion.context.runner=Geçerli işi yürüten koşucu hakkında bilgi. completion.secret.githubToken=Her iş akışı çalıştırması için otomatik olarak oluşturulan belirteç. +completion.secret.giteaToken=Her workflow çalışması için otomatik Gitea tokenı. +completion.env.gitea=Gitea runner ortam değişkeni. +completion.expressionFunction=İfade fonksiyonu. Küçük mantık büyüsü. completion.remote.repository=Uzak depo completion.uses.local.workflow=Yerel yeniden kullanılabilir iş akışı completion.uses.local.action=Yerel eylem @@ -317,6 +322,7 @@ completion.workflow.job.services=Sepet servis konteynerleri completion.workflow.job.uses=Aramak için yeniden kullanılabilir iş akışı completion.workflow.job.with=İş akışı adı verilen girişler completion.workflow.job.secrets=İş akışının sırları +completion.workflow.job.gitea.ignored=Gitea kabul eder, çalışma anında yok sayar. completion.workflow.defaultsRun.shell=Çalıştırma adımları için varsayılan kabuk completion.workflow.defaultsRun.working-directory=Varsayılan çalışma dizini completion.workflow.concurrency.group=Sıraya alınmış çalıştırmalar için kilit adı diff --git a/src/main/resources/messages/GitHubWorkflowBundle_uk.properties b/src/main/resources/messages/GitHubWorkflowBundle_uk.properties index 0587461..0e6858e 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_uk.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_uk.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Невідомий дозвіл [{0 inspection.workflow.syntax.unknownPermissionValue=Невідоме значення дозволу [{0}] inspection.workflow.syntax.unknownJobKey=Невідомий ключ завдання [{0}] inspection.workflow.syntax.unknownStepKey=Невідома клавіша кроку [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea підтримує тут одну мітку runner +inspection.workflow.syntax.giteaExpressionFunction=Gitea документує тут лише always(): [{0}] inspection.action.reload=Перезавантажити [{0}] inspection.action.unresolved=Невирішено [{0}] — перевірте доступ до облікового запису GitHub, дозволи приватного сховища, обмеження швидкості, відсутні посилання або відсутні метадані дії/робочого процесу inspection.action.jump=Перейти до файлу [{0}] @@ -215,6 +217,9 @@ completion.context.github=Запуск робочого процесу та ін completion.context.gitea=Gitea-сумісний псевдонім для контексту дій GitHub. completion.context.runner=Інформація про бігуна, який виконує поточне завдання. completion.secret.githubToken=Автоматично створений маркер для кожного запуску робочого процесу. +completion.secret.giteaToken=Автоматично створений токен Gitea для кожного запуску. +completion.env.gitea=Змінна середовища runner Gitea. +completion.expressionFunction=Функція виразу. Маленьке закляття логіки. completion.remote.repository=Віддалений репозиторій completion.uses.local.workflow=Локальний багаторазовий робочий процес completion.uses.local.action=Місцева дія @@ -317,6 +322,7 @@ completion.workflow.job.services=Сервісні контейнери з кол completion.workflow.job.uses=Багаторазовий робочий процес для виклику completion.workflow.job.with=Вхідні дані для викликаного робочого процесу completion.workflow.job.secrets=Секрети робочого процесу +completion.workflow.job.gitea.ignored=Gitea приймає, під час виконання ігнорує. completion.workflow.defaultsRun.shell=Стандартна оболонка для виконання кроків completion.workflow.defaultsRun.working-directory=Робочий каталог за замовчуванням completion.workflow.concurrency.group=Назва блокування для виконання в черзі diff --git a/src/main/resources/messages/GitHubWorkflowBundle_vi.properties b/src/main/resources/messages/GitHubWorkflowBundle_vi.properties index 0f6c0ca..229d01b 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_vi.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_vi.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=Quyền không xác định [{0}] inspection.workflow.syntax.unknownPermissionValue=Giá trị quyền không xác định [{0}] inspection.workflow.syntax.unknownJobKey=Mã công việc không xác định [{0}] inspection.workflow.syntax.unknownStepKey=Khóa bước không xác định [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea chỉ hỗ trợ một nhãn runner ở đây +inspection.workflow.syntax.giteaExpressionFunction=Gitea chỉ ghi tài liệu always() ở đây: [{0}] inspection.action.reload=Tải lại [{0}] inspection.action.unresolved=[{0}] chưa được giải quyết - kiểm tra quyền truy cập tài khoản GitHub, quyền của kho lưu trữ riêng, giới hạn tốc độ, thiếu giới thiệu hoặc thiếu siêu dữ liệu hành động/quy trình công việc inspection.action.jump=Chuyển tới tập tin [{0}] @@ -215,6 +217,9 @@ completion.context.github=Thông tin sự kiện và hoạt động của quy tr completion.context.gitea=Bí danh tương thích Gitea cho bối cảnh Hành động GitHub. completion.context.runner=Thông tin về người chạy đang thực hiện công việc hiện tại. completion.secret.githubToken=Mã thông báo được tạo tự động cho mỗi lần chạy quy trình công việc. +completion.secret.giteaToken=Token Gitea tự tạo cho mỗi lần chạy workflow. +completion.env.gitea=Biến môi trường của runner Gitea. +completion.expressionFunction=Hàm biểu thức. Bùa logic tí hon. completion.remote.repository=Kho lưu trữ từ xa completion.uses.local.workflow=Quy trình làm việc có thể tái sử dụng cục bộ completion.uses.local.action=Hành động cục bộ @@ -317,6 +322,7 @@ completion.workflow.job.services=Thùng dịch vụ sidecar completion.workflow.job.uses=Quy trình làm việc có thể tái sử dụng để gọi completion.workflow.job.with=Đầu vào cho quy trình công việc được gọi completion.workflow.job.secrets=Bí mật cho quy trình làm việc được gọi là +completion.workflow.job.gitea.ignored=Gitea chấp nhận, runtime bỏ qua. completion.workflow.defaultsRun.shell=Shell mặc định cho các bước chạy completion.workflow.defaultsRun.working-directory=Thư mục làm việc mặc định completion.workflow.concurrency.group=Khóa tên cho các lượt chạy xếp hàng diff --git a/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties b/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties index 4c4d1ec..bdc56cc 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties @@ -85,6 +85,8 @@ inspection.workflow.syntax.unknownPermission=未知权限 [{0}] inspection.workflow.syntax.unknownPermissionValue=未知的权限值 [{0}] inspection.workflow.syntax.unknownJobKey=未知作业密钥 [{0}] inspection.workflow.syntax.unknownStepKey=未知步骤键 [{0}] +inspection.workflow.syntax.giteaRunsOnSingleLabel=Gitea 此处只支持一个 runner 标签 +inspection.workflow.syntax.giteaExpressionFunction=Gitea 此处仅记录 always():[{0}] inspection.action.reload=重新加载[{0}] inspection.action.unresolved=未解决的 [{0}] - 检查 GitHub 帐户访问、私有存储库权限、速率限制、缺少引用或缺少操作/工作流元数据 inspection.action.jump=跳转到文件 [{0}] @@ -215,6 +217,9 @@ completion.context.github=来自 GitHub 上下文的工作流运行和事件信 completion.context.gitea=GitHub 操作上下文的 Gitea 兼容别名。 completion.context.runner=有关执行当前作业的运行程序的信息。 completion.secret.githubToken=为每个工作流程运行自动创建令牌。 +completion.secret.giteaToken=每次工作流运行自动创建的 Gitea 令牌。 +completion.env.gitea=Gitea runner 环境变量。 +completion.expressionFunction=表达式函数。小型逻辑法术。 completion.remote.repository=远程存储库 completion.uses.local.workflow=本地可重用工作流程 completion.uses.local.action=当地行动 @@ -317,6 +322,7 @@ completion.workflow.job.services=边车服务容器 completion.workflow.job.uses=可重用的工作流程调用 completion.workflow.job.with=被调用工作流程的输入 completion.workflow.job.secrets=被调用工作流程的秘密 +completion.workflow.job.gitea.ignored=Gitea 接受,但运行时忽略。 completion.workflow.defaultsRun.shell=运行步骤的默认 shell completion.workflow.defaultsRun.working-directory=默认工作目录 completion.workflow.concurrency.group=排队运行的锁定名称 diff --git a/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java b/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java index a0e4574..737cfb6 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java @@ -11,6 +11,8 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; import java.util.Base64; import java.util.List; @@ -196,17 +198,27 @@ private static String encode(final String value) { } private static String run(final String... command) throws IOException, InterruptedException { - final Process process = new ProcessBuilder(command) - .redirectErrorStream(true) - .start(); - final String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - if (!process.waitFor(60, java.util.concurrent.TimeUnit.SECONDS)) { - process.destroyForcibly(); - throw new IOException("Command timed out: " + String.join(" ", command)); - } - if (process.exitValue() != 0) { - throw new IOException("Command failed [" + String.join(" ", command) + "]: " + output); + final Path errorLog = Files.createTempFile("gitea-docker-", ".err"); + try { + final Process process = new ProcessBuilder(command) + .redirectError(errorLog.toFile()) + .start(); + final String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + if (!process.waitFor(60, java.util.concurrent.TimeUnit.SECONDS)) { + process.destroyForcibly(); + throw new IOException("Command timed out [" + String.join(" ", command) + "]: " + commandOutput(output, errorLog)); + } + if (process.exitValue() != 0) { + throw new IOException("Command failed [" + String.join(" ", command) + "]: " + commandOutput(output, errorLog)); + } + return output; + } finally { + Files.deleteIfExists(errorLog); } - return output; + } + + private static String commandOutput(final String output, final Path errorLog) throws IOException { + final String error = Files.readString(errorLog, StandardCharsets.UTF_8); + return (output + System.lineSeparator() + error).trim(); } } diff --git a/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java b/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java index 3f8f2f1..f86b81a 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java @@ -289,6 +289,7 @@ public void testWorkflowSyntaxCompletionDescriptionsResolveForEveryLocale() { "completion.workflow.permission.shorthand.empty", "completion.workflow.job.runs-on", "completion.workflow.job.steps", + "completion.workflow.job.gitea.ignored", "completion.workflow.step.uses", "completion.workflow.step.run", "completion.workflow.defaultsRun.shell", @@ -304,7 +305,12 @@ public void testWorkflowSyntaxCompletionDescriptionsResolveForEveryLocale() { "completion.workflow.credentials.password", "completion.workflow.inputType.choice", "completion.workflow.boolean.true", - "completion.workflow.runner.ubuntu-latest" + "completion.workflow.runner.ubuntu-latest", + "completion.secret.giteaToken", + "completion.env.gitea", + "completion.expressionFunction", + "inspection.workflow.syntax.giteaRunsOnSingleLabel", + "inspection.workflow.syntax.giteaExpressionFunction" ); for (final String suffix : LOCALE_SUFFIXES) { final Locale locale = Locale.forLanguageTag(suffix.replace('_', '-')); diff --git a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java index 2daaaa8..8f7023b 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java @@ -392,6 +392,18 @@ public void testEnvCompletionIncludesWorkflowJobStepAndRunEnvironmentValues() { """)).contains("WORKFLOW_LEVEL", "JOB_LEVEL", "STEP_LEVEL", "RUN_LEVEL"); } + public void testGiteaRunEnvironmentCompletionIncludesGiteaRunnerVariables() { + assertThat(completeGiteaWorkflow(""" + name: Completion + on: workflow_dispatch + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo "$GITEA" + """)).contains("GITEA_OUTPUT", "GITEA_ENV", "GITEA_STEP_SUMMARY"); + } + public void testEnvCompletionIncludesJobEnvMapAliasValues() { assertThat(completeWorkflow(""" name: Completion @@ -744,6 +756,19 @@ public void testWorkflowCallSecretDefinitionCompletionSuggestsSecretProperties() """)).contains("description", "required"); } + public void testGiteaSecretCompletionSuggestsGiteaToken() { + assertThat(completeGiteaWorkflow(""" + name: Completion + on: workflow_dispatch + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo "${{ secrets. }}" + """)).contains("GITEA_TOKEN") + .doesNotContain("GITHUB_TOKEN"); + } + public void testWorkflowDispatchInputTypeCompletionSuggestsDispatchTypes() { assertThat(completeWorkflow(""" name: Completion @@ -892,6 +917,19 @@ public void testGiteaScheduleCompletionSuggestsCronAliases() { """)).contains("@yearly", "@monthly", "@weekly", "@daily", "@hourly"); } + public void testGiteaRootExpressionCompletionKeepsDocumentedFunctionSetSmall() { + assertThat(completeGiteaWorkflow(""" + name: Completion + on: workflow_dispatch + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo "${{ }}" + """)).contains("always()") + .doesNotContain("success()", "failure()", "cancelled()", "hashFiles()", "startsWith()"); + } + public void testIdTokenPermissionCompletionSuggestsOnlyWriteOrNone() { assertThat(completeWorkflow(""" name: Completion @@ -930,6 +968,18 @@ public void testJobSyntaxCompletionSuggestsDocumentedJobKeys() { """)).contains("runs-on", "permissions", "environment", "strategy", "container", "services", "uses"); } + public void testGiteaIgnoredJobKeysExplainRuntimeNoop() { + assertThat(completeGiteaWorkflowTypeTexts(""" + name: Completion + on: workflow_dispatch + jobs: + build: + + """)).containsEntry("timeout-minutes", "Accepted by Gitea, ignored at runtime.") + .containsEntry("continue-on-error", "Accepted by Gitea, ignored at runtime.") + .containsEntry("environment", "Accepted by Gitea, ignored at runtime."); + } + public void testWorkflowSyntaxCompletionDescriptionsUseConfiguredLanguageAfterEnglishTableUse() { assertThat(completeWorkflowTypeTexts(""" name: Completion diff --git a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java index 12d4bcf..2dbda2f 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java @@ -344,6 +344,43 @@ public void testGiteaRejectsGithubOnlyPermissionScopes() { """); } + public void testGiteaWarnsAboutMultipleRunsOnLabels() { + assertGiteaWorkflowHighlights(""" + name: Syntax + on: workflow_dispatch + jobs: + build: + runs-on: [self-hosted, linux] + steps: + - run: echo ok + """); + } + + public void testGiteaAcceptsSingleRunsOnLabelArray() { + assertGiteaWorkflowHighlights(""" + name: Syntax + on: workflow_dispatch + jobs: + build: + runs-on: [ubuntu-latest] + steps: + - run: echo ok + """); + } + + public void testGiteaWarnsAboutUnsupportedExpressionFunctions() { + assertGiteaWorkflowHighlights(""" + name: Syntax + on: workflow_dispatch + jobs: + build: + if: always() && startsWith(gitea.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - run: echo "${{ hashFiles('package.json') }}" + """); + } + public void testResolvedActionInputIsAccepted() { seedRemoteAction("owner/tool@v1", Map.of("known-input", "Known input"), Map.of()); From df318f078053fbd0d685af57dddf88189b4bb374 Mon Sep 17 00:00:00 2001 From: Yuna Morgenstern Date: Mon, 15 Jun 2026 09:31:12 +0200 Subject: [PATCH 4/8] Harden Gitea workflow runs --- .github/workflows/build.yml | 14 +- .../gitea-github-actions-compatibility.md | 2 +- .../entry/WorkflowAnnotator.java | 26 ++ .../git/RemoteActionProviders.java | 222 ++++++++++++++++-- .../githubworkflow/run/WorkflowRun.java | 143 +++++++++-- .../run/WorkflowRunProcessHandler.java | 21 +- .../GitHubWorkflowSettingsConfigurable.java | 221 ++++++++++++++++- .../messages/GitHubWorkflowBundle.properties | 17 +- .../GitHubWorkflowBundle_ar.properties | 17 +- .../GitHubWorkflowBundle_cs.properties | 17 +- .../GitHubWorkflowBundle_de.properties | 17 +- .../GitHubWorkflowBundle_es.properties | 17 +- .../GitHubWorkflowBundle_fr.properties | 17 +- .../GitHubWorkflowBundle_hi.properties | 17 +- .../GitHubWorkflowBundle_id.properties | 17 +- .../GitHubWorkflowBundle_it.properties | 17 +- .../GitHubWorkflowBundle_ja.properties | 17 +- .../GitHubWorkflowBundle_ko.properties | 17 +- .../GitHubWorkflowBundle_nl.properties | 17 +- .../GitHubWorkflowBundle_pl.properties | 17 +- .../GitHubWorkflowBundle_pt_BR.properties | 17 +- .../GitHubWorkflowBundle_ru.properties | 17 +- .../GitHubWorkflowBundle_sv.properties | 17 +- .../GitHubWorkflowBundle_th.properties | 17 +- .../GitHubWorkflowBundle_tr.properties | 17 +- .../GitHubWorkflowBundle_uk.properties | 17 +- .../GitHubWorkflowBundle_vi.properties | 17 +- .../GitHubWorkflowBundle_zh_CN.properties | 17 +- .../git/RemoteActionProvidersTest.java | 92 +++++++- .../i18n/WorkflowMessagesTest.java | 3 +- .../githubworkflow/run/WorkflowRunTest.java | 86 ++++++- .../syntax/WorkflowValidationTest.java | 12 + 32 files changed, 1127 insertions(+), 72 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97ebf1a..0655e38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -315,11 +315,11 @@ jobs: fi curl "$@" https://plugins.jetbrains.com/api/updates/upload - - name: 📤 Push release commit and tag + - name: 📤 Push release commit + id: release_source if: steps.plan.outputs.release == 'true' && steps.plan.outputs.dry_run != 'true' shell: sh env: - RELEASE_TAG: ${{ steps.plan.outputs.tag }} RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | git config user.name "Kira" @@ -330,9 +330,9 @@ jobs: fi target_branch="${GITHUB_REF_NAME:-main}" + release_target="$(git rev-parse HEAD)" git push origin "HEAD:$target_branch" - git tag -a "$RELEASE_TAG" -m "$RELEASE_TAG [skip ci]" - git push origin "refs/tags/$RELEASE_TAG" + echo "target=$release_target" >> "$GITHUB_OUTPUT" - name: 🚀 Create GitHub release if: steps.plan.outputs.release == 'true' && steps.plan.outputs.dry_run != 'true' @@ -341,12 +341,16 @@ jobs: ARCHIVE_PATH: ${{ steps.archive.outputs.path }} GH_TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }} RELEASE_TAG: ${{ steps.plan.outputs.tag }} + RELEASE_TARGET: ${{ steps.release_source.outputs.target }} run: | if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then gh release upload "$RELEASE_TAG" "$ARCHIVE_PATH" --clobber gh release edit "$RELEASE_TAG" --title "$RELEASE_TAG" --notes-file release-notes.md else - gh release create "$RELEASE_TAG" "$ARCHIVE_PATH" --title "$RELEASE_TAG" --notes-file release-notes.md --verify-tag + if ! gh release create "$RELEASE_TAG" "$ARCHIVE_PATH" --title "$RELEASE_TAG" --notes-file release-notes.md --target "$RELEASE_TARGET"; then + gh release delete "$RELEASE_TAG" --yes --cleanup-tag >/dev/null 2>&1 || true + exit 1 + fi fi - name: 💾 Save cache diff --git a/doc/spec/gitea-github-actions-compatibility.md b/doc/spec/gitea-github-actions-compatibility.md index bf099e4..361913d 100644 --- a/doc/spec/gitea-github-actions-compatibility.md +++ b/doc/spec/gitea-github-actions-compatibility.md @@ -26,7 +26,7 @@ shared behavior first and branch only where Gitea really differs. Tiny forks, no | API auth header | `Authorization: Bearer ...` | `Authorization: token ...` | Gitea providers and runs use `token ...`; GitHub keeps IDE account plus bearer token priority. | | Token env fallback | `GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_PAT` | `GITEA_TOKEN`, `GITEA_PAT` | Gitea runs and metadata use Gitea token env names before anonymous access. | | Workflow dispatch result | GitHub returns run details on current API version | Gitea returns run details when `return_run_details=true` | Gitea dispatch adds `return_run_details=true`; both parse `workflow_run_id`, `run_url`, and `html_url`. | -| Run discovery | Workflow-scoped or repository-scoped run listing | Repository-scoped `actions/runs` with `limit` | GitHub keeps workflow-scoped discovery; Gitea uses repo-level discovery with `limit=1`. | +| Run discovery | Workflow-scoped or repository-scoped run listing | Repository-scoped `actions/runs` with `limit` and run `path` | GitHub keeps workflow-scoped discovery; Gitea reads a small repo-level window and picks the same workflow closest to dispatch time. | | Job logs | `/actions/jobs/{job_id}/logs` | Same path in Gitea OpenAPI | Existing job log download path is shared. | | Run jobs | `/actions/runs/{run}/jobs` | Same path in Gitea OpenAPI | Existing job tree polling path is shared. | | Artifacts | Run artifact list plus artifact ZIP | Same core paths in Gitea OpenAPI | Existing artifact list/download path is shared. | diff --git a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java index 215b842..460a5a7 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowAnnotator.java @@ -478,6 +478,9 @@ private static void validateGiteaExpressionRange(final AnnotationHolder holder, final String body = element.getText().substring(relativeRange.getStartOffset(), relativeRange.getEndOffset()); final Matcher matcher = EXPRESSION_FUNCTION.matcher(body); while (matcher.find()) { + if (insideQuotedString(body, matcher.start(1))) { + continue; + } final String name = matcher.group(1); if (!GITHUB_EXPRESSION_FUNCTIONS.contains(name) || GITEA_EXPRESSION_FUNCTIONS.contains(name)) { continue; @@ -494,6 +497,29 @@ private static void validateGiteaExpressionRange(final AnnotationHolder holder, } } + private static boolean insideQuotedString(final String text, final int offset) { + char quote = 0; + for (int index = 0; index < text.length() && index < offset; index++) { + final char current = text.charAt(index); + if (quote == '\'') { + if (current == '\'' && index + 1 < text.length() && text.charAt(index + 1) == '\'') { + index++; + } else if (current == '\'') { + quote = 0; + } + } else if (quote == '"') { + if (current == '\\' && index + 1 < text.length()) { + index++; + } else if (current == '"') { + quote = 0; + } + } else if (current == '\'' || current == '"') { + quote = current; + } + } + return quote != 0; + } + private static void highlightContext( final AnnotationHolder holder, final LeafPsiElement element, diff --git a/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java b/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java index ad0dfba..2b32d49 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java @@ -6,11 +6,20 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.intellij.credentialStore.CredentialAttributes; +import com.intellij.credentialStore.Credentials; +import com.intellij.ide.passwordSafe.PasswordSafe; import com.intellij.ide.impl.ProjectUtil; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; +import com.intellij.util.xmlb.XmlSerializerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.github.authentication.GHAccountsUtil; import org.jetbrains.plugins.github.authentication.accounts.GithubAccount; import org.jetbrains.plugins.github.util.GHCompatibilityUtil; @@ -31,7 +40,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; import java.util.function.Predicate; @@ -371,20 +379,142 @@ static Optional parse(final Server server, final String value) } } - public static class Settings { + @State(name = "GitHubWorkflowRemoteSettings", storages = {@Storage("githubWorkflowRemoteSettings.xml")}) + public static class Settings implements PersistentStateComponent { public static final String TYPE_GITHUB = "github"; public static final String TYPE_GITEA = "gitea"; - private final CopyOnWriteArrayList testServers = new CopyOnWriteArrayList<>(); + public static class StateData { + public List servers = new ArrayList<>(); + } + + public static class ServerState { + public String type = TYPE_GITHUB; + public String name = ""; + public String webUrl = ""; + public String apiUrl = ""; + public String tokenEnvVar = ""; + public boolean enabled = true; + + public ServerState() { + // XML serializer + } + + ServerState(final Server server) { + final Server normalized = server.normalized(); + type = normalized.type; + name = normalized.name; + webUrl = normalized.webUrl; + apiUrl = normalized.apiUrl; + tokenEnvVar = normalized.tokenEnvVar; + enabled = normalized.enabled; + } + + Server server() { + return new Server(type, name, webUrl, apiUrl, tokenEnvVar, enabled).normalized(); + } + } + + private final StateData state = new StateData(); public static Settings getInstance() { return ApplicationManager.getApplication().getService(Settings.class); } + @Override + public @Nullable StateData getState() { + return state; + } + + @Override + public void loadState(@NotNull final StateData state) { + XmlSerializerUtil.copyBean(state, this.state); + if (this.state.servers == null) { + this.state.servers = new ArrayList<>(); + } + } + + /** + * Returns user-configured remote providers stored by this plugin. + * + * @return normalized configured providers, excluding invalid rows + */ + public List customServers() { + return Optional.ofNullable(state.servers).orElseGet(List::of).stream() + .map(ServerState::server) + .filter(Server::isValid) + .toList(); + } + + /** + * Replaces the user-configured remote providers stored by this plugin. + * + * @param servers provider metadata to persist; invalid rows are ignored + * @return this settings service + */ + public Settings setCustomServers(final List servers) { + state.servers = Optional.ofNullable(servers).orElseGet(List::of).stream() + .map(Server::normalized) + .filter(Server::isValid) + .map(ServerState::new) + .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); + return this; + } + + /** + * Stores or replaces the Gitea token for the supplied provider in the IDE password safe. + * + * @param server provider whose normalized API URL identifies the token + * @param token personal access token + * @return this settings service + */ + public Settings setGiteaToken(final Server server, final String token) { + final Server normalized = Optional.ofNullable(server).orElseGet(Settings::defaultGitHub).normalized(); + if (normalized.isGitea() && hasText(token)) { + PasswordTokens.set(normalized, token); + } + return this; + } + + /** + * Deletes the Gitea token for the supplied provider from the IDE password safe. + * + * @param server provider whose normalized API URL identifies the token + * @return this settings service + */ + public Settings clearGiteaToken(final Server server) { + final Server normalized = Optional.ofNullable(server).orElseGet(Settings::defaultGitHub).normalized(); + if (normalized.isGitea()) { + PasswordTokens.clear(normalized); + } + return this; + } + + /** + * Reads the Gitea token for the supplied provider from the IDE password safe. + * + * @param server provider whose normalized API URL identifies the token + * @return token when one is stored and readable + */ + public Optional giteaToken(final Server server) { + final Server normalized = Optional.ofNullable(server).orElseGet(Settings::defaultGitHub).normalized(); + return normalized.isGitea() ? PasswordTokens.get(normalized) : Optional.empty(); + } + + /** + * Reports whether the supplied Gitea provider has an IDE-stored token. + * + * @param server provider whose normalized API URL identifies the token + * @return true when a token is stored + */ + public boolean hasGiteaToken(final Server server) { + return giteaToken(server).isPresent(); + } + public List enabledServers() { final Map result = new LinkedHashMap<>(); - testServers.stream() + customServers().stream() .map(Server::normalized) .filter(Server::isValid) .forEach(server -> result.put(server.key(), server)); @@ -394,14 +524,6 @@ public List enabledServers() { return List.copyOf(result.values()); } - public void setCustomServers(final List servers) { - testServers.clear(); - Optional.ofNullable(servers).orElseGet(List::of).stream() - .map(Server::normalized) - .filter(Server::isValid) - .forEach(testServers::add); - } - public static Server defaultGitHub() { return new Server("GitHub", "https://github.com", "https://api.github.com", "", true); } @@ -431,6 +553,41 @@ private static List jetBrainsGithubServers() { private static int accountOrder(final GithubAccount account) { return account.getServer().isGithubDotCom() ? 0 : 1; } + + private static class PasswordTokens { + + private static final String SERVICE_PREFIX = "GitHub Workflow Gitea "; + + private static Optional get(final Server server) { + try { + return Optional.ofNullable(PasswordSafe.getInstance().get(attributes(server))) + .map(Credentials::getPasswordAsString) + .filter(RemoteActionProviders::hasText); + } catch (final RuntimeException ignored) { + return Optional.empty(); + } + } + + private static void set(final Server server, final String token) { + try { + PasswordSafe.getInstance().set(attributes(server), new Credentials(server.name, token)); + } catch (final RuntimeException exception) { + LOG.warn("Gitea token storage failed for [" + server.apiUrl + "]", exception); + } + } + + private static void clear(final Server server) { + try { + PasswordSafe.getInstance().set(attributes(server), null); + } catch (final RuntimeException exception) { + LOG.warn("Gitea token deletion failed for [" + server.apiUrl + "]", exception); + } + } + + private static CredentialAttributes attributes(final Server server) { + return new CredentialAttributes(SERVICE_PREFIX + server.normalized().apiUrl); + } + } } public static class Server { @@ -528,10 +685,11 @@ public boolean isGitea() { } public String authorizationHeader() { - return Optional.ofNullable(tokenEnvVar) - .filter(RemoteActionProviders::hasText) - .map(System::getenv) - .filter(RemoteActionProviders::hasText) + return Settings.getInstance().giteaToken(this) + .or(() -> Optional.ofNullable(tokenEnvVar) + .filter(RemoteActionProviders::hasText) + .map(System::getenv) + .filter(RemoteActionProviders::hasText)) .map(token -> authorizationPrefix() + token) .orElse(""); } @@ -636,6 +794,8 @@ public static List forServer( final Server normalized = Optional.ofNullable(server).orElseGet(Settings::defaultGitHub).normalized(); if (normalized.isGitea()) { final LinkedHashMap result = new LinkedHashMap<>(); + giteaAccountAuthorizations(normalized) + .forEach(authorization -> result.putIfAbsent(authorization.key(), authorization)); envAuthorizations(normalized.tokenEnvVar, environment, DEFAULT_GITEA_ENV_TOKENS, "token ") .forEach(authorization -> result.putIfAbsent(authorization.key(), authorization)); result.putIfAbsent(Authorization.anonymous().key(), Authorization.anonymous()); @@ -661,8 +821,36 @@ private static List forGithubApiUrl( return List.copyOf(result.values()); } + public static String settingsHint(final Server server) { + final Server normalized = Optional.ofNullable(server).orElseGet(Settings::defaultGitHub).normalized(); + return normalized.isGitea() + ? GitHubWorkflowBundle.message("workflow.run.auth.settings.gitea") + : GitHubWorkflowBundle.message("workflow.run.auth.settings.github"); + } + public static String settingsHint() { - return GitHubWorkflowBundle.message("workflow.run.auth.settings"); + return GitHubWorkflowBundle.message("workflow.run.auth.settings.github"); + } + + private static List giteaAccountAuthorizations(final Server server) { + final Settings settings = Settings.getInstance(); + return settings.enabledServers().stream() + .filter(Server::isGitea) + .sorted(Comparator + .comparingInt((Server candidate) -> giteaAccountPriority(candidate, server)) + .thenComparing(candidate -> candidate.apiUrl) + .thenComparing(candidate -> candidate.name)) + .map(candidate -> settings.giteaToken(candidate) + .map(token -> new Authorization(candidate.name, "token " + token))) + .flatMap(Optional::stream) + .toList(); + } + + private static int giteaAccountPriority(final Server candidate, final Server server) { + if (candidate.normalized().apiUrl.equals(server.normalized().apiUrl)) { + return 0; + } + return sameHost(candidate.apiUrl, server.apiUrl) ? 1 : 2; } private static List orderedAccountsFor(final String apiUrl) { diff --git a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java index 3ff5def..4428c94 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java @@ -36,7 +36,10 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeParseException; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -45,7 +48,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Stream; @@ -175,7 +177,28 @@ public DeleteResult delete(final Request request, final long runId) throws IOExc return new DeleteResult(response.statusCode(), accepted(response)); } + /** + * Finds the most likely run created by a workflow dispatch when the dispatch response did not include a run id. + * + * @param request workflow repository and branch context + * @return the closest matching workflow dispatch run, or an empty result when no safe candidate exists + * @throws IOException when the remote API rejects the request or the network call fails + * @throws InterruptedException when the IDE cancels the remote call + */ public Optional latestRun(final Request request) throws IOException, InterruptedException { + return latestRun(request, Instant.now()); + } + + /** + * Finds the most likely run created near one dispatch timestamp. + * + * @param request workflow repository and branch context + * @param dispatchTime local timestamp captured immediately before dispatching the workflow + * @return the closest same-workflow run near the dispatch timestamp, or an empty result when no candidate fits + * @throws IOException when the remote API rejects the request or the network call fails + * @throws InterruptedException when the IDE cancels the remote call + */ + public Optional latestRun(final Request request, final Instant dispatchTime) throws IOException, InterruptedException { final HttpResponse response = send( request, "GET", @@ -184,10 +207,15 @@ public Optional latestRun(final Request request) throws IOException, "GitHub workflow run discovery" ); final JsonObject json = parseObject(response.body()); + final Instant baseTime = Optional.ofNullable(dispatchTime).orElse(Instant.EPOCH); + final Instant earliest = baseTime.minusSeconds(30); return objects(json, "workflow_runs") - .findFirst() - .map(run -> runStatus(run, -1L)) - .filter(run -> run.runId() >= 0); + .map(run -> runCandidate(request, run)) + .filter(candidate -> candidate.status().runId() >= 0) + .filter(RunCandidate::workflowMatches) + .filter(candidate -> candidate.timestamp().map(timestamp -> !timestamp.isBefore(earliest)).orElse(true)) + .min(runCandidateComparator(baseTime)) + .map(RunCandidate::status); } public String logs(final Request request, final long runId) throws IOException, InterruptedException { @@ -308,7 +336,7 @@ private HttpResponse sendWithAuthorizations( final String body, final String operation, final ResponseSender sender, - final BiFunction, WorkflowRunHttpException> failureFactory, + final FailureFactory failureFactory, final Function, String> bodyText ) throws IOException, InterruptedException { WorkflowRunHttpException lastFailure = null; @@ -325,7 +353,7 @@ private HttpResponse sendWithAuthorizations( } return response; } - lastFailure = failureFactory.apply(operation, response); + lastFailure = failureFactory.failure(workflow, operation, response); if (authorization.authenticated() && rateLimitExceeded(response.statusCode(), response.headers(), bodyText.apply(response))) { authenticatedRateLimitFailure = true; } @@ -393,31 +421,38 @@ private static HttpRequest request( return builder.build(); } - private static WorkflowRunHttpException failure(final String operation, final HttpResponse response) { - return failure(operation, response.statusCode(), response.headers(), response.body()); + private static WorkflowRunHttpException failure(final Request request, final String operation, final HttpResponse response) { + return failure(request, operation, response.statusCode(), response.headers(), response.body()); } - private static WorkflowRunHttpException failureBytes(final String operation, final HttpResponse response) { + private static WorkflowRunHttpException failureBytes(final Request request, final String operation, final HttpResponse response) { final String body = new String(Optional.ofNullable(response.body()).orElseGet(() -> new byte[0]), StandardCharsets.UTF_8); - return failure(operation, response.statusCode(), response.headers(), body); + return failure(request, operation, response.statusCode(), response.headers(), body); } private static WorkflowRunHttpException failure( + final Request request, final String operation, final int statusCode, final HttpHeaders headers, final String body ) { final boolean accountActionRecommended = needsAccountAction(statusCode, headers, body); + final RemoteActionProviders.Server server = RemoteActionProviders.Server.fromWorkflowRun( + request.apiUrl(), + request.workflowPath(), + request.tokenEnvVar() + ); final String hint = accountActionRecommended - ? "\nAdd or refresh GitHub accounts in " + RemoteActionProviders.Authorizations.settingsHint() + "." + ? "\n" + GitHubWorkflowBundle.message("workflow.run.auth.add", RemoteActionProviders.Authorizations.settingsHint(server)) : ""; final String summary = responseSummary(statusCode, headers, body); return new WorkflowRunHttpException( operation + " failed with HTTP " + statusCode + (summary.isEmpty() ? "" : ": " + summary) + hint, statusCode, body, - accountActionRecommended + accountActionRecommended, + server.isGitea() ? "github.workflow.settings" : "GitHub" ); } @@ -497,7 +532,7 @@ private static String latestRunsUrl(final Request request) { final String baseUrl = server.isGitea() ? request.apiUrl() + "/repos/" + encode(request.owner()) + "/" + encode(request.repo()) + "/actions/runs" : workflowUrl(request) + "/runs"; - final String pageLimit = server.isGitea() ? "limit=1" : "per_page=1"; + final String pageLimit = server.isGitea() ? "limit=20" : "per_page=20"; return baseUrl + "?branch=" + encode(request.ref()) + "&event=workflow_dispatch&" + pageLimit; } @@ -559,6 +594,72 @@ private static RunStatus runStatus(final JsonObject object, final long fallbackR ); } + private static RunCandidate runCandidate(final Request request, final JsonObject object) { + return new RunCandidate( + runStatus(object, -1L), + runTimestamp(object), + workflowMatches(request, object) + ); + } + + private static Comparator runCandidateComparator(final Instant dispatchTime) { + return Comparator + .comparingInt((RunCandidate candidate) -> candidate.timestamp().isPresent() ? 0 : 1) + .thenComparingLong(candidate -> candidate.timestamp() + .map(timestamp -> instantDistanceMillis(timestamp, dispatchTime)) + .orElse(Long.MAX_VALUE)); + } + + private static long instantDistanceMillis(final Instant left, final Instant right) { + try { + return Math.abs(Duration.between(left, right).toMillis()); + } catch (final ArithmeticException ignored) { + return Long.MAX_VALUE; + } + } + + private static Optional runTimestamp(final JsonObject object) { + return Stream.of("created_at", "run_started_at", "started_at", "updated_at") + .flatMap(name -> stringValue(object, name).stream()) + .flatMap(value -> parseInstant(value).stream()) + .filter(timestamp -> timestamp.isAfter(Instant.parse("2000-01-01T00:00:00Z"))) + .findFirst(); + } + + private static Optional parseInstant(final String value) { + try { + return Optional.of(Instant.parse(value)); + } catch (final DateTimeParseException ignored) { + return Optional.empty(); + } + } + + private static boolean workflowMatches(final Request request, final JsonObject object) { + if (!server(request).isGitea()) { + return true; + } + return Stream.of("path", "workflow_path", "workflow_file_path") + .flatMap(name -> stringValue(object, name).stream()) + .findFirst() + .map(path -> sameWorkflowPath(request.workflowPath(), path)) + .orElse(true); + } + + private static boolean sameWorkflowPath(final String expected, final String actual) { + final String normalizedExpected = normalizeWorkflowPath(expected); + final String normalizedActual = normalizeWorkflowPath(actual); + return normalizedExpected.equals(normalizedActual) + || workflowId(normalizedExpected).equals(workflowId(normalizedActual)); + } + + private static String normalizeWorkflowPath(final String value) { + String normalized = Optional.ofNullable(value).orElse("").replace('\\', '/').strip(); + while (normalized.startsWith("./")) { + normalized = normalized.substring(2); + } + return normalized; + } + private static JobStatus jobStatus(final JsonObject object) { return new JobStatus( longValue(object, "id").orElse(-1L), @@ -637,6 +738,10 @@ private interface ResponseSender { HttpResponse send(HttpRequest request) throws IOException, InterruptedException; } + private interface FailureFactory { + WorkflowRunHttpException failure(Request request, String operation, HttpResponse response); + } + private record JdkHttpTransport(HttpClient client) implements HttpTransport { @Override public HttpResponse send(final HttpRequest request) throws IOException, InterruptedException { @@ -1031,6 +1136,9 @@ public boolean completed() { } } + private record RunCandidate(RunStatus status, Optional timestamp, boolean workflowMatches) { + } + public record CancelResult(int statusCode, boolean accepted) { } @@ -1051,17 +1159,20 @@ public static class WorkflowRunHttpException extends IOException { private final int statusCode; private final String body; private final boolean accountActionRecommended; + private final String settingsId; public WorkflowRunHttpException( final String message, final int statusCode, final String body, - final boolean accountActionRecommended + final boolean accountActionRecommended, + final String settingsId ) { super(message); this.statusCode = statusCode; this.body = body; this.accountActionRecommended = accountActionRecommended; + this.settingsId = settingsId; } public int statusCode() { @@ -1075,5 +1186,9 @@ public String body() { public boolean accountActionRecommended() { return accountActionRecommended; } + + public String settingsId() { + return settingsId; + } } } diff --git a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java index db2966d..0ce3860 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java @@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -157,7 +158,7 @@ private void runWorkflow() { terminate(runFromTrigger()); } catch (final IOException | RuntimeException exception) { if (exception instanceof WorkflowRun.WorkflowRunHttpException httpException && httpException.accountActionRecommended()) { - notifyAuthenticationHelp(); + notifyAuthenticationHelp(httpException.settingsId()); } stderr(exception.getMessage() + "\n"); terminate(1, "failure"); @@ -180,10 +181,11 @@ private RunOutcome runFromTrigger() throws IOException, InterruptedException { private long dispatchFromTrigger() throws IOException, InterruptedException { stdout(dispatchMessage() + "\n"); - return resolveRunId(client.dispatch(request)); + final Instant dispatchTime = Instant.now(); + return resolveRunId(client.dispatch(request), dispatchTime); } - private long resolveRunId(final WorkflowRun.DispatchResult dispatch) throws IOException, InterruptedException { + private long resolveRunId(final WorkflowRun.DispatchResult dispatch, final Instant dispatchTime) throws IOException, InterruptedException { if (hasText(dispatch.htmlUrl())) { stdout(GitHubWorkflowBundle.message("workflow.run.link", dispatch.htmlUrl()) + "\n"); } @@ -192,7 +194,7 @@ private long resolveRunId(final WorkflowRun.DispatchResult dispatch) throws IOEx } stdout(GitHubWorkflowBundle.message("workflow.run.discovery") + "\n"); for (int attempt = 0; attempt < 12 && !stopping.get(); attempt++) { - final var latest = client.latestRun(request); + final var latest = client.latestRun(request, dispatchTime); if (latest.isPresent()) { final WorkflowRun.RunStatus run = latest.get(); if (hasText(run.htmlUrl())) { @@ -530,16 +532,21 @@ private void terminate(final RunOutcome outcome) { terminate(outcome.exitCode(), outcome.conclusion()); } - private void notifyAuthenticationHelp() { + private void notifyAuthenticationHelp(final String settingsId) { final var notification = NotificationGroupManager.getInstance() .getNotificationGroup("GitHub Workflow") .createNotification( - GitHubWorkflowBundle.message("workflow.run.notification.auth", RemoteActionProviders.Authorizations.settingsHint()), + GitHubWorkflowBundle.message( + "workflow.run.notification.auth", + "github.workflow.settings".equals(settingsId) + ? GitHubWorkflowBundle.message("workflow.run.auth.settings.gitea") + : GitHubWorkflowBundle.message("workflow.run.auth.settings.github") + ), NotificationType.WARNING ); notification.addAction(NotificationAction.createSimple(GitHubWorkflowBundle.message("workflow.run.notification.openSettings"), () -> ApplicationManager.getApplication().invokeLater(() -> - ShowSettingsUtil.getInstance().showSettingsDialog(project, "GitHub")))); + ShowSettingsUtil.getInstance().showSettingsDialog(project, settingsId)))); notification.notify(project); } diff --git a/src/main/java/com/github/yunabraska/githubworkflow/settings/GitHubWorkflowSettingsConfigurable.java b/src/main/java/com/github/yunabraska/githubworkflow/settings/GitHubWorkflowSettingsConfigurable.java index bcc1753..5427436 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/settings/GitHubWorkflowSettingsConfigurable.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/settings/GitHubWorkflowSettingsConfigurable.java @@ -2,6 +2,7 @@ import com.github.yunabraska.githubworkflow.state.GitHubActionCache; +import com.github.yunabraska.githubworkflow.git.RemoteActionProviders; import com.github.yunabraska.githubworkflow.i18n.GitHubWorkflowBundle; import com.intellij.ide.BrowserUtil; @@ -33,9 +34,14 @@ import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Arrays; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.ThreadLocalRandom; /** @@ -44,6 +50,14 @@ public class GitHubWorkflowSettingsConfigurable implements SearchableConfigurable { private static final String SUPPORT_URL = "https://github.com/sponsors/YunaBraska"; + private static final String STORED_TOKEN = "********"; + private static final int GITEA_ENABLED = 0; + private static final int GITEA_NAME = 1; + private static final int GITEA_WEB_URL = 2; + private static final int GITEA_API_URL = 3; + private static final int GITEA_TOKEN_ENV = 4; + private static final int GITEA_TOKEN = 5; + private static final int GITEA_ROW_ID = 6; private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault()); private static final List LOCALES = List.of( new LocaleOption(GitHubWorkflowBundle.Settings.SYSTEM_LANGUAGE, "settings.language.system", true), @@ -70,8 +84,24 @@ public class GitHubWorkflowSettingsConfigurable implements SearchableConfigurabl ); private final GitHubWorkflowBundle.Settings settings = GitHubWorkflowBundle.Settings.getInstance(); + private final RemoteActionProviders.Settings remoteSettings = RemoteActionProviders.Settings.getInstance(); private final GitHubActionCache cache = GitHubActionCache.getActionCache(); private final JComboBox language = new JComboBox<>(LOCALES.toArray(LocaleOption[]::new)); + private final DefaultTableModel giteaModel = new DefaultTableModel() { + @Override + public Class getColumnClass(final int columnIndex) { + return columnIndex == GITEA_ENABLED ? Boolean.class : String.class; + } + + @Override + public boolean isCellEditable(final int row, final int column) { + return column != GITEA_TOKEN && column != GITEA_ROW_ID; + } + }; + private final JTable giteaTable = new JBTable(giteaModel); + private final Map pendingGiteaTokens = new HashMap<>(); + private final Set clearedGiteaTokens = new HashSet<>(); + private int nextGiteaRowSequence = 1; private final DefaultTableModel tableModel = new DefaultTableModel(); private final JTable table = new JBTable(tableModel); private final JLabel summary = new JLabel(); @@ -92,7 +122,10 @@ public class GitHubWorkflowSettingsConfigurable implements SearchableConfigurabl panel = new JPanel(new BorderLayout(8, 8)); panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); panel.add(topPanel(), BorderLayout.NORTH); - panel.add(cachePanel(), BorderLayout.CENTER); + final JPanel center = new JPanel(new BorderLayout(8, 8)); + center.add(giteaPanel(), BorderLayout.NORTH); + center.add(cachePanel(), BorderLayout.CENTER); + panel.add(center, BorderLayout.CENTER); reset(); return panel; } @@ -100,13 +133,18 @@ public class GitHubWorkflowSettingsConfigurable implements SearchableConfigurabl @Override public boolean isModified() { final LocaleOption option = (LocaleOption) language.getSelectedItem(); - return option != null && !Objects.equals(option.tag(), settings.languageTag()); + return option != null && !Objects.equals(option.tag(), settings.languageTag()) + || !Objects.equals(giteaServersFromTable(), remoteSettings.customServers().stream() + .filter(RemoteActionProviders.Server::isGitea) + .toList()) + || giteaTokenModified(); } @Override public void apply() throws ConfigurationException { final LocaleOption option = (LocaleOption) language.getSelectedItem(); settings.languageTag(option == null ? GitHubWorkflowBundle.Settings.SYSTEM_LANGUAGE : option.tag()); + saveGiteaRows(); reloadTable(); GitHubActionCache.triggerSyntaxHighlightingForActiveFiles(); } @@ -114,6 +152,7 @@ public void apply() throws ConfigurationException { @Override public void reset() { selectLanguage(settings.languageTag()); + reloadGiteaTable(); reloadTable(); } @@ -149,6 +188,37 @@ private JPanel topPanel() { return result; } + private JPanel giteaPanel() { + giteaModel.setColumnIdentifiers(new Object[]{ + GitHubWorkflowBundle.message("settings.gitea.column.enabled"), + GitHubWorkflowBundle.message("settings.gitea.column.name"), + GitHubWorkflowBundle.message("settings.gitea.column.webUrl"), + GitHubWorkflowBundle.message("settings.gitea.column.apiUrl"), + GitHubWorkflowBundle.message("settings.gitea.column.tokenEnv"), + GitHubWorkflowBundle.message("settings.gitea.column.token"), + "" + }); + if (giteaTable.getColumnModel().getColumnCount() > GITEA_ROW_ID) { + giteaTable.removeColumn(giteaTable.getColumnModel().getColumn(GITEA_ROW_ID)); + } + giteaTable.setSelectionMode(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + + final JPanel result = new JPanel(new BorderLayout(4, 4)); + result.setBorder(BorderFactory.createTitledBorder(GitHubWorkflowBundle.message("settings.gitea.title"))); + result.add(new JScrollPane(giteaTable), BorderLayout.CENTER); + result.add(giteaButtons(), BorderLayout.SOUTH); + return result; + } + + private JPanel giteaButtons() { + final JPanel result = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); + addButton(result, "settings.gitea.add", this::addGiteaRow); + addButton(result, "settings.gitea.remove", this::removeSelectedGiteaRows); + addButton(result, "settings.gitea.setToken", this::setSelectedGiteaToken); + addButton(result, "settings.gitea.clearToken", this::clearSelectedGiteaToken); + return result; + } + private JPanel cachePanel() { tableModel.setColumnIdentifiers(new Object[]{ GitHubWorkflowBundle.message("settings.cache.column.key"), @@ -183,6 +253,153 @@ private void addButton(final JPanel panel, final String key, final Runnable acti panel.add(button); } + private void reloadGiteaTable() { + pendingGiteaTokens.clear(); + clearedGiteaTokens.clear(); + giteaModel.setRowCount(0); + remoteSettings.customServers().stream() + .filter(RemoteActionProviders.Server::isGitea) + .forEach(server -> giteaModel.addRow(new Object[]{ + server.enabled, + server.name, + server.webUrl, + server.apiUrl, + server.tokenEnvVar, + remoteSettings.hasGiteaToken(server) ? STORED_TOKEN : "", + nextGiteaRowId() + })); + } + + private void addGiteaRow() { + giteaModel.addRow(new Object[]{ + true, + "Gitea", + "https://gitea.com", + "https://gitea.com/api/v1", + "GITEA_TOKEN", + "", + nextGiteaRowId() + }); + } + + private void removeSelectedGiteaRows() { + final int[] rows = giteaTable.getSelectedRows(); + for (int index = rows.length - 1; index >= 0; index--) { + giteaModel.removeRow(giteaTable.convertRowIndexToModel(rows[index])); + } + } + + private void setSelectedGiteaToken() { + final int row = selectedGiteaRow(); + if (row < 0) { + Messages.showInfoMessage(panel, GitHubWorkflowBundle.message("settings.gitea.noneSelected"), getDisplayName()); + return; + } + final String token = Messages.showPasswordDialog(GitHubWorkflowBundle.message("settings.gitea.token.prompt"), getDisplayName()); + if (token != null && !token.isBlank()) { + final int rowId = rowId(row); + pendingGiteaTokens.put(rowId, token); + clearedGiteaTokens.remove(rowId); + giteaModel.setValueAt(STORED_TOKEN, row, GITEA_TOKEN); + } + } + + private void clearSelectedGiteaToken() { + final int row = selectedGiteaRow(); + if (row < 0) { + Messages.showInfoMessage(panel, GitHubWorkflowBundle.message("settings.gitea.noneSelected"), getDisplayName()); + return; + } + final int rowId = rowId(row); + pendingGiteaTokens.remove(rowId); + clearedGiteaTokens.add(rowId); + giteaModel.setValueAt("", row, GITEA_TOKEN); + } + + private int selectedGiteaRow() { + final int viewRow = giteaTable.getSelectedRow(); + return viewRow < 0 ? -1 : giteaTable.convertRowIndexToModel(viewRow); + } + + private void saveGiteaRows() { + final List previous = remoteSettings.customServers(); + final List servers = giteaServersFromTable(); + final List nextKeys = servers.stream().map(server -> server.apiUrl).toList(); + final List persisted = new ArrayList<>(previous.stream() + .filter(server -> !server.isGitea()) + .toList()); + persisted.addAll(servers); + + previous.stream() + .filter(RemoteActionProviders.Server::isGitea) + .filter(server -> !nextKeys.contains(server.apiUrl)) + .forEach(remoteSettings::clearGiteaToken); + + for (int row = 0; row < giteaModel.getRowCount(); row++) { + final RemoteActionProviders.Server server = giteaServerFromRow(row); + if (!server.isValid()) { + continue; + } + final int rowId = rowId(row); + if (pendingGiteaTokens.containsKey(rowId)) { + remoteSettings.setGiteaToken(server, pendingGiteaTokens.get(rowId)); + } else if (clearedGiteaTokens.contains(rowId)) { + remoteSettings.clearGiteaToken(server); + } + } + remoteSettings.setCustomServers(persisted); + reloadGiteaTable(); + } + + private boolean giteaTokenModified() { + if (!pendingGiteaTokens.isEmpty()) { + return true; + } + for (int row = 0; row < giteaModel.getRowCount(); row++) { + final RemoteActionProviders.Server server = giteaServerFromRow(row); + if (clearedGiteaTokens.contains(rowId(row)) && remoteSettings.hasGiteaToken(server)) { + return true; + } + } + return false; + } + + private List giteaServersFromTable() { + final List result = new ArrayList<>(); + for (int row = 0; row < giteaModel.getRowCount(); row++) { + final RemoteActionProviders.Server server = giteaServerFromRow(row); + if (server.isValid()) { + result.add(server); + } + } + return result; + } + + private RemoteActionProviders.Server giteaServerFromRow(final int row) { + return RemoteActionProviders.Server.gitea( + text(row, GITEA_NAME), + text(row, GITEA_WEB_URL), + text(row, GITEA_API_URL), + text(row, GITEA_TOKEN_ENV), + Boolean.TRUE.equals(giteaModel.getValueAt(row, GITEA_ENABLED)) + ).normalized(); + } + + private String text(final int row, final int column) { + return Objects.toString(giteaModel.getValueAt(row, column), "").trim(); + } + + private int rowId(final int row) { + final Object value = giteaModel.getValueAt(row, GITEA_ROW_ID); + return value instanceof Integer id ? id : -1; + } + + private int nextGiteaRowId() { + final int next = nextGiteaRowSequence; + nextGiteaRowSequence++; + return next; + } + private void reloadTable() { tableModel.setRowCount(0); cache.entries().forEach(entry -> tableModel.addRow(new Object[]{ diff --git a/src/main/resources/messages/GitHubWorkflowBundle.properties b/src/main/resources/messages/GitHubWorkflowBundle.properties index 66f0df2..1008785 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub returned an HTML error page instead of API data. workflow.run.gutter.stop=Stop workflow run workflow.run.gutter.stop.text=Stop workflow run workflow.run.gutter.stop.description=Cancel this run -workflow.run.auth.settings=Settings > Version Control > GitHub +workflow.run.auth.settings.github=Settings > Version Control > GitHub +workflow.run.auth.settings.gitea=Settings > Tools > GitHub Workflow +workflow.run.auth.add=Add a token or account in {0}. workflow.log.command=run: workflow.log.warning=warning: workflow.log.error=error: @@ -384,6 +386,19 @@ completion.jobs.result=The result of the job. settings.displayName=GitHub Workflow settings.language.label=Language: settings.language.system=IDE/system default +settings.gitea.title=Gitea accounts +settings.gitea.column.enabled=On +settings.gitea.column.name=Name +settings.gitea.column.webUrl=Web URL +settings.gitea.column.apiUrl=API URL +settings.gitea.column.tokenEnv=Token env +settings.gitea.column.token=Token +settings.gitea.add=Add Gitea +settings.gitea.remove=Remove +settings.gitea.setToken=Set token +settings.gitea.clearToken=Clear token +settings.gitea.noneSelected=Select a Gitea row first. +settings.gitea.token.prompt=Paste Gitea token settings.cache.title=Action cache settings.cache.column.key=Cache key settings.cache.column.name=Name diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ar.properties b/src/main/resources/messages/GitHubWorkflowBundle_ar.properties index 5ea92c0..084e3cf 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ar.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ar.properties @@ -29,7 +29,9 @@ workflow.run.error.html=أعاد GitHub صفحة خطأ HTML بدلًا من ب workflow.run.gutter.stop=إيقاف تشغيل سير العمل workflow.run.gutter.stop.text=إيقاف تشغيل سير العمل workflow.run.gutter.stop.description=إلغاء هذا التشغيل -workflow.run.auth.settings=الإعدادات > التحكم بالإصدارات > GitHub +workflow.run.auth.settings.github=الإعدادات > التحكم بالإصدار > GitHub +workflow.run.auth.settings.gitea=الإعدادات > الأدوات > GitHub Workflow +workflow.run.auth.add=أضف رمزا أو حسابا في {0}. workflow.log.command=يجري: workflow.log.warning=تحذير: workflow.log.error=خطأ: @@ -384,6 +386,19 @@ completion.jobs.result=نتيجة الوظيفة. settings.displayName=سير العمل GitHub settings.language.label=لغة: settings.language.system=IDE/النظام الافتراضي +settings.gitea.title=حسابات Gitea +settings.gitea.column.enabled=مفعل +settings.gitea.column.name=الاسم +settings.gitea.column.webUrl=رابط الويب +settings.gitea.column.apiUrl=رابط API +settings.gitea.column.tokenEnv=متغير الرمز +settings.gitea.column.token=رمز الوصول +settings.gitea.add=أضف Gitea +settings.gitea.remove=إزالة +settings.gitea.setToken=تعيين الرمز +settings.gitea.clearToken=مسح الرمز +settings.gitea.noneSelected=اختر صف Gitea أولا. +settings.gitea.token.prompt=الصق رمز Gitea settings.cache.title=ذاكرة التخزين المؤقت للعمل settings.cache.column.key=مفتاح ذاكرة التخزين المؤقت settings.cache.column.name=اسم diff --git a/src/main/resources/messages/GitHubWorkflowBundle_cs.properties b/src/main/resources/messages/GitHubWorkflowBundle_cs.properties index eab63e9..e02167f 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_cs.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_cs.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub vrátil chybovou stránku HTML místo dat API. workflow.run.gutter.stop=Zastavit běh pracovního postupu workflow.run.gutter.stop.text=Zastavit běh pracovního postupu workflow.run.gutter.stop.description=Zrušte tento běh -workflow.run.auth.settings=Nastavení > Správa verzí > GitHub +workflow.run.auth.settings.github=Nastavení > Správa verzí > GitHub +workflow.run.auth.settings.gitea=Nastavení > Nástroje > GitHub Workflow +workflow.run.auth.add=Přidej token nebo účet v {0}. workflow.log.command=spustit: workflow.log.warning=varování: workflow.log.error=chyba: @@ -384,6 +386,19 @@ completion.jobs.result=Výsledek práce. settings.displayName=Pracovní postup GitHub settings.language.label=jazyk: settings.language.system=IDE/výchozí nastavení systému +settings.gitea.title=Účty Gitea +settings.gitea.column.enabled=Zapnuto +settings.gitea.column.name=Název +settings.gitea.column.webUrl=Webová URL +settings.gitea.column.apiUrl=API URL adresa +settings.gitea.column.tokenEnv=Token proměnná +settings.gitea.column.token=Přístupový token +settings.gitea.add=Přidat Gitea +settings.gitea.remove=Odebrat +settings.gitea.setToken=Nastavit token +settings.gitea.clearToken=Smazat token +settings.gitea.noneSelected=Nejdřív vyber řádek Gitea. +settings.gitea.token.prompt=Vlož token Gitea settings.cache.title=Mezipaměť akcí settings.cache.column.key=Klíč mezipaměti settings.cache.column.name=Jméno diff --git a/src/main/resources/messages/GitHubWorkflowBundle_de.properties b/src/main/resources/messages/GitHubWorkflowBundle_de.properties index 5cc94cd..94f24f1 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_de.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_de.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub gab eine HTML-Fehlerseite statt API-Daten zurück workflow.run.gutter.stop=Stoppen Sie die Workflow-Ausführung workflow.run.gutter.stop.text=Stoppen Sie die Workflow-Ausführung workflow.run.gutter.stop.description=Brechen Sie diesen Lauf ab -workflow.run.auth.settings=Einstellungen > Versionskontrolle > GitHub +workflow.run.auth.settings.github=Einstellungen > Versionskontrolle > GitHub +workflow.run.auth.settings.gitea=Einstellungen > Tools > GitHub Workflow +workflow.run.auth.add=Token oder Konto in {0} hinzufügen. workflow.log.command=Lauf: workflow.log.warning=Warnung: workflow.log.error=Fehler: @@ -384,6 +386,19 @@ completion.jobs.result=Das Ergebnis der Arbeit. settings.displayName=GitHub-Workflow settings.language.label=Sprache: settings.language.system=IDE/Systemstandard +settings.gitea.title=Gitea-Konten +settings.gitea.column.enabled=Aktiv +settings.gitea.column.name=Bezeichnung +settings.gitea.column.webUrl=Web-URL +settings.gitea.column.apiUrl=API-URL +settings.gitea.column.tokenEnv=Token-Env +settings.gitea.column.token=Zugriffstoken +settings.gitea.add=Gitea hinzufügen +settings.gitea.remove=Entfernen +settings.gitea.setToken=Token setzen +settings.gitea.clearToken=Token löschen +settings.gitea.noneSelected=Erst eine Gitea-Zeile wählen. +settings.gitea.token.prompt=Gitea-Token einfügen settings.cache.title=Aktionscache settings.cache.column.key=Cache-Schlüssel settings.cache.column.name=Bezeichnung diff --git a/src/main/resources/messages/GitHubWorkflowBundle_es.properties b/src/main/resources/messages/GitHubWorkflowBundle_es.properties index 525226d..ec2b66c 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_es.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_es.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub devolvió una página de error HTML en lugar de d workflow.run.gutter.stop=Detener la ejecución del flujo de trabajo workflow.run.gutter.stop.text=Detener la ejecución del flujo de trabajo workflow.run.gutter.stop.description=Cancelar esta ejecución -workflow.run.auth.settings=Configuración > Control de versiones > GitHub +workflow.run.auth.settings.github=Ajustes > Control de versiones > GitHub +workflow.run.auth.settings.gitea=Ajustes > Herramientas > GitHub Workflow +workflow.run.auth.add=Añade un token o cuenta en {0}. workflow.log.command=ejecutar: workflow.log.warning=advertencia: workflow.log.error=error: @@ -384,6 +386,19 @@ completion.jobs.result=El resultado del trabajo. settings.displayName=Flujo de trabajo GitHub settings.language.label=Idioma: settings.language.system=IDE/valor predeterminado del sistema +settings.gitea.title=Cuentas de Gitea +settings.gitea.column.enabled=Activo +settings.gitea.column.name=Nombre +settings.gitea.column.webUrl=URL web +settings.gitea.column.apiUrl=URL de API +settings.gitea.column.tokenEnv=Env del token +settings.gitea.column.token=Token de acceso +settings.gitea.add=Añadir Gitea +settings.gitea.remove=Quitar +settings.gitea.setToken=Guardar token +settings.gitea.clearToken=Borrar token +settings.gitea.noneSelected=Selecciona primero una fila Gitea. +settings.gitea.token.prompt=Pega el token de Gitea settings.cache.title=Caché de acciones settings.cache.column.key=clave de caché settings.cache.column.name=Nombre diff --git a/src/main/resources/messages/GitHubWorkflowBundle_fr.properties b/src/main/resources/messages/GitHubWorkflowBundle_fr.properties index c8f382d..4c48367 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_fr.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_fr.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub a renvoyé une page d’erreur HTML au lieu des d workflow.run.gutter.stop=Arrêter l''exécution du workflow workflow.run.gutter.stop.text=Arrêter l''exécution du workflow workflow.run.gutter.stop.description=Annuler cette course -workflow.run.auth.settings=Paramètres > Contrôle de version > GitHub +workflow.run.auth.settings.github=Paramètres > Contrôle de version > GitHub +workflow.run.auth.settings.gitea=Paramètres > Outils > GitHub Workflow +workflow.run.auth.add=Ajoutez un jeton ou un compte dans {0}. workflow.log.command=exécuter : workflow.log.warning=avertissement : workflow.log.error=erreur : @@ -384,6 +386,19 @@ completion.jobs.result=Le résultat du travail. settings.displayName=Flux de travail GitHub settings.language.label=Langue : settings.language.system=IDE/système par défaut +settings.gitea.title=Comptes Gitea +settings.gitea.column.enabled=Actif +settings.gitea.column.name=Nom +settings.gitea.column.webUrl=URL web +settings.gitea.column.apiUrl=URL API +settings.gitea.column.tokenEnv=Env du jeton +settings.gitea.column.token=Jeton accès +settings.gitea.add=Ajouter Gitea +settings.gitea.remove=Retirer +settings.gitea.setToken=Définir jeton +settings.gitea.clearToken=Effacer jeton +settings.gitea.noneSelected=Sélectionnez d’abord une ligne Gitea. +settings.gitea.token.prompt=Collez le jeton Gitea settings.cache.title=Cache d''actions settings.cache.column.key=Clé de cache settings.cache.column.name=Nom diff --git a/src/main/resources/messages/GitHubWorkflowBundle_hi.properties b/src/main/resources/messages/GitHubWorkflowBundle_hi.properties index 71ab692..abc8e28 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_hi.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_hi.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub ने API डेटा के बजाय HTML workflow.run.gutter.stop=वर्कफ़्लो चलाना बंद करें workflow.run.gutter.stop.text=वर्कफ़्लो चलाना बंद करें workflow.run.gutter.stop.description=यह रन रद्द करें -workflow.run.auth.settings=सेटिंग्स > संस्करण नियंत्रण > GitHub +workflow.run.auth.settings.github=सेटिंग्स > वर्शन कंट्रोल > GitHub +workflow.run.auth.settings.gitea=सेटिंग्स > टूल्स > GitHub Workflow +workflow.run.auth.add={0} में टोकन या खाता जोड़ें. workflow.log.command=चलाएँ: workflow.log.warning=चेतावनी: workflow.log.error=त्रुटि: @@ -384,6 +386,19 @@ completion.jobs.result=कार्य का परिणाम. settings.displayName=GitHub वर्कफ़्लो settings.language.label=भाषा: settings.language.system=IDE/सिस्टम डिफ़ॉल्ट +settings.gitea.title=Gitea खाते +settings.gitea.column.enabled=चालू +settings.gitea.column.name=नाम +settings.gitea.column.webUrl=वेब URL +settings.gitea.column.apiUrl=API URL पता +settings.gitea.column.tokenEnv=टोकन env +settings.gitea.column.token=पहुंच टोकन +settings.gitea.add=Gitea जोड़ें +settings.gitea.remove=हटाएं +settings.gitea.setToken=टोकन सेट करें +settings.gitea.clearToken=टोकन साफ करें +settings.gitea.noneSelected=पहले Gitea पंक्ति चुनें. +settings.gitea.token.prompt=Gitea टोकन चिपकाएं settings.cache.title=एक्शन कैश settings.cache.column.key=कैश कुंजी settings.cache.column.name=नाम diff --git a/src/main/resources/messages/GitHubWorkflowBundle_id.properties b/src/main/resources/messages/GitHubWorkflowBundle_id.properties index 5d2a4c5..1500a3b 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_id.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_id.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub mengembalikan halaman galat HTML, bukan data API. workflow.run.gutter.stop=Hentikan alur kerja yang dijalankan workflow.run.gutter.stop.text=Hentikan alur kerja yang dijalankan workflow.run.gutter.stop.description=Batalkan proses ini -workflow.run.auth.settings=Pengaturan > Kontrol Versi > GitHub +workflow.run.auth.settings.github=Pengaturan > Kontrol Versi > GitHub +workflow.run.auth.settings.gitea=Pengaturan > Alat > GitHub Workflow +workflow.run.auth.add=Tambahkan token atau akun di {0}. workflow.log.command=menjalankan: workflow.log.warning=peringatan: workflow.log.error=kesalahan: @@ -384,6 +386,19 @@ completion.jobs.result=Hasil pekerjaan. settings.displayName=Alur Kerja GitHub settings.language.label=Bahasa: settings.language.system=IDE/default sistem +settings.gitea.title=Akun Gitea +settings.gitea.column.enabled=Aktif +settings.gitea.column.name=Nama +settings.gitea.column.webUrl=URL web +settings.gitea.column.apiUrl=URL API +settings.gitea.column.tokenEnv=Env token +settings.gitea.column.token=Token akses +settings.gitea.add=Tambah Gitea +settings.gitea.remove=Hapus +settings.gitea.setToken=Atur token +settings.gitea.clearToken=Bersihkan token +settings.gitea.noneSelected=Pilih baris Gitea dulu. +settings.gitea.token.prompt=Tempel token Gitea settings.cache.title=Tembolok tindakan settings.cache.column.key=Kunci cache settings.cache.column.name=Nama diff --git a/src/main/resources/messages/GitHubWorkflowBundle_it.properties b/src/main/resources/messages/GitHubWorkflowBundle_it.properties index 2673e7f..8642cb9 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_it.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_it.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub ha restituito una pagina di errore HTML invece de workflow.run.gutter.stop=Arresta l''esecuzione del flusso di lavoro workflow.run.gutter.stop.text=Arresta l''esecuzione del flusso di lavoro workflow.run.gutter.stop.description=Annulla questa corsa -workflow.run.auth.settings=Impostazioni > Controllo versione > GitHub +workflow.run.auth.settings.github=Impostazioni > Controllo versione > GitHub +workflow.run.auth.settings.gitea=Impostazioni > Strumenti > GitHub Workflow +workflow.run.auth.add=Aggiungi token o account in {0}. workflow.log.command=correre: workflow.log.warning=avvertimento: workflow.log.error=errore: @@ -384,6 +386,19 @@ completion.jobs.result=Il risultato del lavoro. settings.displayName=Flusso di lavoro GitHub settings.language.label=Lingua: settings.language.system=IDE/impostazione predefinita del sistema +settings.gitea.title=Account Gitea +settings.gitea.column.enabled=Attivo +settings.gitea.column.name=Nome +settings.gitea.column.webUrl=URL web +settings.gitea.column.apiUrl=URL API +settings.gitea.column.tokenEnv=Env token +settings.gitea.column.token=Token accesso +settings.gitea.add=Aggiungi Gitea +settings.gitea.remove=Rimuovi +settings.gitea.setToken=Imposta token +settings.gitea.clearToken=Cancella token +settings.gitea.noneSelected=Seleziona prima una riga Gitea. +settings.gitea.token.prompt=Incolla token Gitea settings.cache.title=Cache delle azioni settings.cache.column.key=Chiave della cache settings.cache.column.name=Nome diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ja.properties b/src/main/resources/messages/GitHubWorkflowBundle_ja.properties index 2ad83d8..6495103 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ja.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ja.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub は API データではなく HTML エラーペ workflow.run.gutter.stop=ワークフローの実行を停止する workflow.run.gutter.stop.text=ワークフローの実行を停止する workflow.run.gutter.stop.description=この実行をキャンセルする -workflow.run.auth.settings=設定 > バージョン管理 > GitHub +workflow.run.auth.settings.github=設定 > バージョン管理 > GitHub +workflow.run.auth.settings.gitea=設定 > ツール > GitHub Workflow +workflow.run.auth.add={0} でトークンまたはアカウントを追加。 workflow.log.command=実行: workflow.log.warning=警告: workflow.log.error=エラー: @@ -384,6 +386,19 @@ completion.jobs.result=仕事の結果。 settings.displayName=GitHub ワークフロー settings.language.label=言語: settings.language.system=IDE/システムデフォルト +settings.gitea.title=Gitea アカウント +settings.gitea.column.enabled=有効 +settings.gitea.column.name=名前 +settings.gitea.column.webUrl=Web URL 値 +settings.gitea.column.apiUrl=API URL 値 +settings.gitea.column.tokenEnv=トークン環境変数 +settings.gitea.column.token=アクセス用トークン +settings.gitea.add=Gitea 追加 +settings.gitea.remove=削除 +settings.gitea.setToken=トークン設定 +settings.gitea.clearToken=トークン消去 +settings.gitea.noneSelected=先に Gitea 行を選択。 +settings.gitea.token.prompt=Gitea トークンを貼り付け settings.cache.title=アクションキャッシュ settings.cache.column.key=キャッシュキー settings.cache.column.name=名前 diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ko.properties b/src/main/resources/messages/GitHubWorkflowBundle_ko.properties index dce0cff..586b67a 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ko.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ko.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub가 API 데이터 대신 HTML 오류 페이지를 workflow.run.gutter.stop=워크플로 실행 중지 workflow.run.gutter.stop.text=워크플로 실행 중지 workflow.run.gutter.stop.description=이 실행 취소 -workflow.run.auth.settings=설정 > 버전 관리 > GitHub +workflow.run.auth.settings.github=설정 > 버전 관리 > GitHub +workflow.run.auth.settings.gitea=설정 > 도구 > GitHub Workflow +workflow.run.auth.add={0}에서 토큰 또는 계정을 추가하세요. workflow.log.command=실행: workflow.log.warning=경고: workflow.log.error=오류: @@ -384,6 +386,19 @@ completion.jobs.result=작업의 결과입니다. settings.displayName=GitHub 작업 흐름 settings.language.label=언어: settings.language.system=IDE/시스템 기본값 +settings.gitea.title=Gitea 계정 +settings.gitea.column.enabled=사용 +settings.gitea.column.name=이름 +settings.gitea.column.webUrl=웹 URL +settings.gitea.column.apiUrl=API URL 값 +settings.gitea.column.tokenEnv=토큰 환경변수 +settings.gitea.column.token=접근 토큰 +settings.gitea.add=Gitea 추가 +settings.gitea.remove=제거 +settings.gitea.setToken=토큰 설정 +settings.gitea.clearToken=토큰 지우기 +settings.gitea.noneSelected=먼저 Gitea 행을 선택하세요. +settings.gitea.token.prompt=Gitea 토큰 붙여넣기 settings.cache.title=액션 캐시 settings.cache.column.key=캐시 키 settings.cache.column.name=이름 diff --git a/src/main/resources/messages/GitHubWorkflowBundle_nl.properties b/src/main/resources/messages/GitHubWorkflowBundle_nl.properties index 4051755..553748c 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_nl.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_nl.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub gaf een HTML-foutpagina terug in plaats van API-g workflow.run.gutter.stop=Stop de werkstroomuitvoering workflow.run.gutter.stop.text=Stop de werkstroomuitvoering workflow.run.gutter.stop.description=Annuleer deze uitvoering -workflow.run.auth.settings=Instellingen > Versiebeheer > GitHub +workflow.run.auth.settings.github=Instellingen > Versiebeheer > GitHub +workflow.run.auth.settings.gitea=Instellingen > Tools > GitHub Workflow +workflow.run.auth.add=Voeg een token of account toe in {0}. workflow.log.command=rennen: workflow.log.warning=waarschuwing: workflow.log.error=fout: @@ -384,6 +386,19 @@ completion.jobs.result=Het resultaat van de klus. settings.displayName=GitHub-workflow settings.language.label=Taal: settings.language.system=IDE/systeemstandaard +settings.gitea.title=Gitea-accounts +settings.gitea.column.enabled=Aan +settings.gitea.column.name=Naam +settings.gitea.column.webUrl=Web-URL +settings.gitea.column.apiUrl=API-URL adres +settings.gitea.column.tokenEnv=Token-env +settings.gitea.column.token=Toegangstoken +settings.gitea.add=Gitea toevoegen +settings.gitea.remove=Verwijderen +settings.gitea.setToken=Token instellen +settings.gitea.clearToken=Token wissen +settings.gitea.noneSelected=Selecteer eerst een Gitea-rij. +settings.gitea.token.prompt=Plak Gitea-token settings.cache.title=Actiecache settings.cache.column.key=Cachesleutel settings.cache.column.name=Naam diff --git a/src/main/resources/messages/GitHubWorkflowBundle_pl.properties b/src/main/resources/messages/GitHubWorkflowBundle_pl.properties index 3e28c8f..f95f955 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_pl.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_pl.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub zwrócił stronę błędu HTML zamiast danych API workflow.run.gutter.stop=Zatrzymaj przebieg przepływu pracy workflow.run.gutter.stop.text=Zatrzymaj przebieg przepływu pracy workflow.run.gutter.stop.description=Anuluj ten bieg -workflow.run.auth.settings=Ustawienia > Kontrola wersji > GitHub +workflow.run.auth.settings.github=Ustawienia > Kontrola wersji > GitHub +workflow.run.auth.settings.gitea=Ustawienia > Narzędzia > GitHub Workflow +workflow.run.auth.add=Dodaj token albo konto w {0}. workflow.log.command=biegnij: workflow.log.warning=ostrzeżenie: workflow.log.error=błąd: @@ -384,6 +386,19 @@ completion.jobs.result=Wynik pracy. settings.displayName=Przebieg pracy GitHub settings.language.label=Język: settings.language.system=Wartość domyślna IDE/systemowa +settings.gitea.title=Konta Gitea +settings.gitea.column.enabled=Włączone +settings.gitea.column.name=Nazwa +settings.gitea.column.webUrl=URL WWW +settings.gitea.column.apiUrl=URL API adres +settings.gitea.column.tokenEnv=Env tokena +settings.gitea.column.token=Token dostępu +settings.gitea.add=Dodaj Gitea +settings.gitea.remove=Usuń +settings.gitea.setToken=Ustaw token +settings.gitea.clearToken=Wyczyść token +settings.gitea.noneSelected=Najpierw wybierz wiersz Gitea. +settings.gitea.token.prompt=Wklej token Gitea settings.cache.title=Pamięć akcji settings.cache.column.key=Klucz pamięci podręcznej settings.cache.column.name=Imię diff --git a/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties b/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties index 8403c4a..829e578 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub retornou uma página de erro HTML em vez de dados workflow.run.gutter.stop=Interromper a execução do fluxo de trabalho workflow.run.gutter.stop.text=Interromper a execução do fluxo de trabalho workflow.run.gutter.stop.description=Cancelar esta execução -workflow.run.auth.settings=Configurações > Controle de versão > GitHub +workflow.run.auth.settings.github=Configurações > Controle de versão > GitHub +workflow.run.auth.settings.gitea=Configurações > Ferramentas > GitHub Workflow +workflow.run.auth.add=Adicione token ou conta em {0}. workflow.log.command=execute: workflow.log.warning=aviso: workflow.log.error=erro: @@ -384,6 +386,19 @@ completion.jobs.result=O resultado do trabalho. settings.displayName=Fluxo de trabalho GitHub settings.language.label=Idioma: settings.language.system=IDE/padrão do sistema +settings.gitea.title=Contas Gitea +settings.gitea.column.enabled=Ativo +settings.gitea.column.name=Nome +settings.gitea.column.webUrl=URL web +settings.gitea.column.apiUrl=URL de API +settings.gitea.column.tokenEnv=Env do token +settings.gitea.column.token=Token de acesso +settings.gitea.add=Adicionar Gitea +settings.gitea.remove=Remover +settings.gitea.setToken=Definir token +settings.gitea.clearToken=Limpar token +settings.gitea.noneSelected=Selecione uma linha Gitea primeiro. +settings.gitea.token.prompt=Cole o token Gitea settings.cache.title=Cache de ação settings.cache.column.key=Chave de cache settings.cache.column.name=Nome diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ru.properties b/src/main/resources/messages/GitHubWorkflowBundle_ru.properties index fd3d737..023410c 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ru.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ru.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub вернул HTML-страницу ошибки workflow.run.gutter.stop=Остановить выполнение рабочего процесса workflow.run.gutter.stop.text=Остановить выполнение рабочего процесса workflow.run.gutter.stop.description=Отменить этот запуск -workflow.run.auth.settings=Настройки > Контроль версий > GitHub +workflow.run.auth.settings.github=Настройки > Контроль версий > GitHub +workflow.run.auth.settings.gitea=Настройки > Инструменты > GitHub Workflow +workflow.run.auth.add=Добавьте токен или аккаунт в {0}. workflow.log.command=запустить: workflow.log.warning=предупреждение: workflow.log.error=ошибка: @@ -384,6 +386,19 @@ completion.jobs.result=Результат работы. settings.displayName=GitHub Рабочий процесс settings.language.label=Язык: settings.language.system=IDE/системное значение по умолчанию +settings.gitea.title=Аккаунты Gitea +settings.gitea.column.enabled=Вкл +settings.gitea.column.name=Имя +settings.gitea.column.webUrl=Web URL адрес +settings.gitea.column.apiUrl=API URL адрес +settings.gitea.column.tokenEnv=Env токена +settings.gitea.column.token=Токен доступа +settings.gitea.add=Добавить Gitea +settings.gitea.remove=Удалить +settings.gitea.setToken=Задать токен +settings.gitea.clearToken=Очистить токен +settings.gitea.noneSelected=Сначала выберите строку Gitea. +settings.gitea.token.prompt=Вставьте токен Gitea settings.cache.title=Кэш действий settings.cache.column.key=Ключ кэша settings.cache.column.name=Имя diff --git a/src/main/resources/messages/GitHubWorkflowBundle_sv.properties b/src/main/resources/messages/GitHubWorkflowBundle_sv.properties index a4987bf..1d165b0 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_sv.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_sv.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub returnerade en HTML-felsida i stället för API-d workflow.run.gutter.stop=Stoppa körningen av arbetsflödet workflow.run.gutter.stop.text=Stoppa körningen av arbetsflödet workflow.run.gutter.stop.description=Avbryt denna körning -workflow.run.auth.settings=Inställningar > Versionskontroll > GitHub +workflow.run.auth.settings.github=Inställningar > Versionskontroll > GitHub +workflow.run.auth.settings.gitea=Inställningar > Verktyg > GitHub Workflow +workflow.run.auth.add=Lägg till token eller konto i {0}. workflow.log.command=kör: workflow.log.warning=varning: workflow.log.error=fel: @@ -384,6 +386,19 @@ completion.jobs.result=Resultatet av jobbet. settings.displayName=GitHub arbetsflöde settings.language.label=Språk: settings.language.system=IDE/systemstandard +settings.gitea.title=Gitea-konton +settings.gitea.column.enabled=På +settings.gitea.column.name=Namn +settings.gitea.column.webUrl=Webbadress +settings.gitea.column.apiUrl=API-adress +settings.gitea.column.tokenEnv=Token-env +settings.gitea.column.token=Åtkomsttoken +settings.gitea.add=Lägg till Gitea +settings.gitea.remove=Ta bort +settings.gitea.setToken=Sätt token +settings.gitea.clearToken=Rensa token +settings.gitea.noneSelected=Välj en Gitea-rad först. +settings.gitea.token.prompt=Klistra in Gitea-token settings.cache.title=Åtgärdscache settings.cache.column.key=Cache-nyckel settings.cache.column.name=Namn diff --git a/src/main/resources/messages/GitHubWorkflowBundle_th.properties b/src/main/resources/messages/GitHubWorkflowBundle_th.properties index 725f495..46a2b73 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_th.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_th.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub ส่งคืนหน้าแสดงข้ workflow.run.gutter.stop=หยุดการรันเวิร์กโฟลว์ workflow.run.gutter.stop.text=หยุดการรันเวิร์กโฟลว์ workflow.run.gutter.stop.description=ยกเลิกการวิ่งครั้งนี้ -workflow.run.auth.settings=การตั้งค่า > การควบคุมเวอร์ชัน > GitHub +workflow.run.auth.settings.github=ตั้งค่า > ควบคุมเวอร์ชัน > GitHub +workflow.run.auth.settings.gitea=ตั้งค่า > เครื่องมือ > GitHub Workflow +workflow.run.auth.add=เพิ่มโทเคนหรือบัญชีใน {0}. workflow.log.command=วิ่ง: workflow.log.warning=คำเตือน: workflow.log.error=ข้อผิดพลาด: @@ -384,6 +386,19 @@ completion.jobs.result=ผลของงาน. settings.displayName=เวิร์กโฟลว์ GitHub settings.language.label=ภาษา: settings.language.system=IDE/ค่าเริ่มต้นของระบบ +settings.gitea.title=บัญชี Gitea +settings.gitea.column.enabled=เปิด +settings.gitea.column.name=ชื่อ +settings.gitea.column.webUrl=URL เว็บ +settings.gitea.column.apiUrl=URL API +settings.gitea.column.tokenEnv=env โทเคน +settings.gitea.column.token=โทเคนเข้าถึง +settings.gitea.add=เพิ่ม Gitea +settings.gitea.remove=ลบ +settings.gitea.setToken=ตั้งโทเคน +settings.gitea.clearToken=ล้างโทเคน +settings.gitea.noneSelected=เลือกแถว Gitea ก่อน. +settings.gitea.token.prompt=วางโทเคน Gitea settings.cache.title=แคชการดำเนินการ settings.cache.column.key=รหัสแคช settings.cache.column.name=ชื่อ diff --git a/src/main/resources/messages/GitHubWorkflowBundle_tr.properties b/src/main/resources/messages/GitHubWorkflowBundle_tr.properties index 8a05287..ae255ff 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_tr.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_tr.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub, API verileri yerine HTML hata sayfası döndürd workflow.run.gutter.stop=İş akışı çalışmasını durdur workflow.run.gutter.stop.text=İş akışı çalışmasını durdur workflow.run.gutter.stop.description=Bu çalıştırmayı iptal et -workflow.run.auth.settings=Ayarlar > Sürüm Denetimi > GitHub +workflow.run.auth.settings.github=Ayarlar > Sürüm Kontrolü > GitHub +workflow.run.auth.settings.gitea=Ayarlar > Araçlar > GitHub Workflow +workflow.run.auth.add={0} içinde token veya hesap ekleyin. workflow.log.command=koş: workflow.log.warning=uyarı: workflow.log.error=hata: @@ -384,6 +386,19 @@ completion.jobs.result=İşin sonucu. settings.displayName=GitHub İş Akışı settings.language.label=Dil: settings.language.system=IDE/sistem varsayılanı +settings.gitea.title=Gitea hesapları +settings.gitea.column.enabled=Açık +settings.gitea.column.name=Ad +settings.gitea.column.webUrl=Web URL adresi +settings.gitea.column.apiUrl=API URL adresi +settings.gitea.column.tokenEnv=Token ortamı +settings.gitea.column.token=Erişim tokeni +settings.gitea.add=Gitea ekle +settings.gitea.remove=Kaldır +settings.gitea.setToken=Token ayarla +settings.gitea.clearToken=Token temizle +settings.gitea.noneSelected=Önce bir Gitea satırı seçin. +settings.gitea.token.prompt=Gitea tokenini yapıştır settings.cache.title=Eylem önbelleği settings.cache.column.key=Önbellek anahtarı settings.cache.column.name=İsim diff --git a/src/main/resources/messages/GitHubWorkflowBundle_uk.properties b/src/main/resources/messages/GitHubWorkflowBundle_uk.properties index 0e6858e..eac1fd6 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_uk.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_uk.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub повернув HTML-сторінку помил workflow.run.gutter.stop=Зупинити запуск робочого процесу workflow.run.gutter.stop.text=Зупинити запуск робочого процесу workflow.run.gutter.stop.description=Скасувати цей запуск -workflow.run.auth.settings=Налаштування > Контроль версій > GitHub +workflow.run.auth.settings.github=Налаштування > Контроль версій > GitHub +workflow.run.auth.settings.gitea=Налаштування > Інструменти > GitHub Workflow +workflow.run.auth.add=Додайте токен або обліковку в {0}. workflow.log.command=запустити: workflow.log.warning=попередження: workflow.log.error=помилка: @@ -384,6 +386,19 @@ completion.jobs.result=Результат роботи. settings.displayName=Робочий процес GitHub settings.language.label=мова: settings.language.system=IDE/система за замовчуванням +settings.gitea.title=Обліковки Gitea +settings.gitea.column.enabled=Увімк +settings.gitea.column.name=Назва +settings.gitea.column.webUrl=Web URL адреса +settings.gitea.column.apiUrl=API URL адреса +settings.gitea.column.tokenEnv=Env токена +settings.gitea.column.token=Токен доступу +settings.gitea.add=Додати Gitea +settings.gitea.remove=Вилучити +settings.gitea.setToken=Задати токен +settings.gitea.clearToken=Очистити токен +settings.gitea.noneSelected=Спершу виберіть рядок Gitea. +settings.gitea.token.prompt=Вставте токен Gitea settings.cache.title=Кеш дій settings.cache.column.key=Ключ кешу settings.cache.column.name=Ім''я diff --git a/src/main/resources/messages/GitHubWorkflowBundle_vi.properties b/src/main/resources/messages/GitHubWorkflowBundle_vi.properties index 229d01b..2fe7b7d 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_vi.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_vi.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub trả về trang lỗi HTML thay vì dữ liệu workflow.run.gutter.stop=Dừng chạy quy trình công việc workflow.run.gutter.stop.text=Dừng chạy quy trình công việc workflow.run.gutter.stop.description=Hủy lần chạy này -workflow.run.auth.settings=Cài đặt > Quản lý phiên bản > GitHub +workflow.run.auth.settings.github=Cài đặt > Quản lý phiên bản > GitHub +workflow.run.auth.settings.gitea=Cài đặt > Công cụ > GitHub Workflow +workflow.run.auth.add=Thêm token hoặc tài khoản trong {0}. workflow.log.command=chạy: workflow.log.warning=cảnh báo: workflow.log.error=lỗi: @@ -384,6 +386,19 @@ completion.jobs.result=Kết quả của công việc. settings.displayName=Quy trình làm việc của GitHub settings.language.label=Ngôn ngữ: settings.language.system=IDE/mặc định hệ thống +settings.gitea.title=Tài khoản Gitea +settings.gitea.column.enabled=Bật +settings.gitea.column.name=Tên +settings.gitea.column.webUrl=URL web +settings.gitea.column.apiUrl=URL API +settings.gitea.column.tokenEnv=Env token +settings.gitea.column.token=Token truy cập +settings.gitea.add=Thêm Gitea +settings.gitea.remove=Xóa +settings.gitea.setToken=Đặt token +settings.gitea.clearToken=Xóa token +settings.gitea.noneSelected=Chọn một dòng Gitea trước. +settings.gitea.token.prompt=Dán token Gitea settings.cache.title=Bộ đệm hành động settings.cache.column.key=Khóa bộ đệm settings.cache.column.name=Tên diff --git a/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties b/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties index bdc56cc..0e6febf 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties @@ -29,7 +29,9 @@ workflow.run.error.html=GitHub 返回了 HTML 错误页面,而不是 API 数 workflow.run.gutter.stop=停止工作流程运行 workflow.run.gutter.stop.text=停止工作流程运行 workflow.run.gutter.stop.description=取消本次运行 -workflow.run.auth.settings=设置 > 版本控制 > GitHub +workflow.run.auth.settings.github=设置 > 版本控制 > GitHub +workflow.run.auth.settings.gitea=设置 > 工具 > GitHub Workflow +workflow.run.auth.add=在 {0} 添加令牌或账户。 workflow.log.command=运行: workflow.log.warning=警告: workflow.log.error=错误: @@ -384,6 +386,19 @@ completion.jobs.result=工作的结果。 settings.displayName=GitHub 工作流程 settings.language.label=语言: settings.language.system=IDE/系统默认 +settings.gitea.title=Gitea 账户 +settings.gitea.column.enabled=启用 +settings.gitea.column.name=名称 +settings.gitea.column.webUrl=网页 URL +settings.gitea.column.apiUrl=API URL 地址 +settings.gitea.column.tokenEnv=令牌环境变量 +settings.gitea.column.token=访问令牌 +settings.gitea.add=添加 Gitea +settings.gitea.remove=移除 +settings.gitea.setToken=设置令牌 +settings.gitea.clearToken=清除令牌 +settings.gitea.noneSelected=先选择一个 Gitea 行。 +settings.gitea.token.prompt=粘贴 Gitea 令牌 settings.cache.title=动作缓存 settings.cache.column.key=缓存键 settings.cache.column.name=名称 diff --git a/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java b/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java index d40dc90..9ea110c 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java @@ -17,7 +17,9 @@ public class RemoteActionProvidersTest extends BasePlatformTestCase { @Override protected void tearDown() throws Exception { try { - RemoteActionProviders.Settings.getInstance().setCustomServers(List.of()); + final RemoteActionProviders.Settings settings = RemoteActionProviders.Settings.getInstance(); + settings.customServers().forEach(settings::clearGiteaToken); + settings.setCustomServers(List.of()); } finally { super.tearDown(); } @@ -300,6 +302,94 @@ public void testGiteaWorkflowRunEnvironmentTokensUseTokenAuthorizationScheme() { .containsExactly("token local-token", "token default-token", ""); } + public void testGiteaStoredTokenIsTriedBeforeEnvironmentTokens() { + final RemoteActionProviders.Server server = RemoteActionProviders.Server.gitea( + "Local Gitea", + "http://gitea.local", + "http://gitea.local/api/v1", + "LOCAL_GITEA_TOKEN", + true + ); + final RemoteActionProviders.Settings settings = RemoteActionProviders.Settings.getInstance() + .setCustomServers(List.of(server)) + .setGiteaToken(server, "stored-token"); + + final List authorizations = RemoteActionProviders.Authorizations.forServer( + server, + null, + Map.of("LOCAL_GITEA_TOKEN", "local-token", "GITEA_TOKEN", "default-token") + ); + + assertThat(settings.hasGiteaToken(server)).isTrue(); + assertThat(authorizations) + .extracting(RemoteActionProviders.Authorizations.Authorization::source) + .containsExactly("Local Gitea", "LOCAL_GITEA_TOKEN", "GITEA_TOKEN", "anonymous"); + assertThat(authorizations) + .extracting(RemoteActionProviders.Authorizations.Authorization::authorizationHeader) + .containsExactly("token stored-token", "token local-token", "token default-token", ""); + } + + public void testGiteaStoredTokenForMatchingServerBeatsOtherGiteaTokens() { + final RemoteActionProviders.Server first = RemoteActionProviders.Server.gitea( + "First Gitea", + "http://first.local", + "http://first.local/api/v1", + "", + true + ); + final RemoteActionProviders.Server second = RemoteActionProviders.Server.gitea( + "Second Gitea", + "http://second.local", + "http://second.local/api/v1", + "", + true + ); + RemoteActionProviders.Settings.getInstance() + .setCustomServers(List.of(first, second)) + .setGiteaToken(first, "first-token") + .setGiteaToken(second, "second-token"); + + final List authorizations = RemoteActionProviders.Authorizations.forWorkflowRun( + "http://second.local/api/v1", + ".gitea/workflows/build.yml", + "", + null, + Map.of() + ); + + assertThat(authorizations) + .extracting(RemoteActionProviders.Authorizations.Authorization::source) + .containsExactly("Second Gitea", "First Gitea", "anonymous"); + assertThat(authorizations) + .extracting(RemoteActionProviders.Authorizations.Authorization::authorizationHeader) + .containsExactly("token second-token", "token first-token", ""); + } + + public void testGiteaStoredTokenIsNotSerializedInSettingsState() { + final RemoteActionProviders.Server server = RemoteActionProviders.Server.gitea( + "Local Gitea", + "http://gitea.local", + "http://gitea.local/api/v1", + "LOCAL_GITEA_TOKEN", + true + ); + + final RemoteActionProviders.Settings settings = RemoteActionProviders.Settings.getInstance() + .setCustomServers(List.of(server)) + .setGiteaToken(server, "stored-token"); + + assertThat(settings.getState().servers) + .hasSize(1) + .allSatisfy(state -> assertThat(List.of( + state.type, + state.name, + state.webUrl, + state.apiUrl, + state.tokenEnvVar + )).doesNotContain("stored-token")); + assertThat(settings.getState().servers.getFirst().tokenEnvVar).isEqualTo("LOCAL_GITEA_TOKEN"); + } + public void testGithubEnvironmentTokensStillUseBearerAuthorizationScheme() { final RemoteActionProviders.Server server = new RemoteActionProviders.Server( "GitHub", diff --git a/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java b/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java index f86b81a..a555b49 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java @@ -144,7 +144,8 @@ public void testLocalizedBundlesDoNotKeepKnownEnglishMergeLeftovers() throws IOE "action.GitHubWorkflow.RefreshActionCache.text", "action.GitHubWorkflow.RefreshActionCache.description", "settings.cache.refresh", - "workflow.run.auth.settings" + "workflow.run.auth.settings.github", + "workflow.run.auth.settings.gitea" ); for (final String suffix : LOCALE_SUFFIXES) { final Properties bundle = loadBundle("_" + suffix); diff --git a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java index f6b204d..21e770c 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java @@ -16,6 +16,7 @@ import java.net.http.HttpResponse; import java.net.http.HttpClient.Version; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -102,13 +103,17 @@ public void testLatestRunDiscoversNewestWorkflowDispatchRun() throws Exception { assertThat(result.get().runId()).isEqualTo(77); assertThat(result.get().status()).isEqualTo("queued"); assertThat(result.get().htmlUrl()).isEqualTo("html-latest"); - assertThat(server.requests()).contains("/repos/acme/tool/actions/workflows/build.yml/runs?branch=feature%2Fone&event=workflow_dispatch&per_page=1"); + assertThat(server.requests()).contains("/repos/acme/tool/actions/workflows/build.yml/runs?branch=feature%2Fone&event=workflow_dispatch&per_page=20"); } } public void testGiteaDispatchRequestsRunDetailsAndLatestRunUsesRepositoryRunsEndpoint() throws Exception { try (FakeWorkflowRunServer server = new FakeWorkflowRunServer()) { - final WorkflowRun client = new WorkflowRun(); + final HttpClient httpClient = HttpClient.newHttpClient(); + final WorkflowRun client = new WorkflowRun( + request -> httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)), + request -> List.of(RemoteActionProviders.Authorizations.Authorization.anonymous()) + ); final WorkflowRun.Request request = new WorkflowRun.Request( server.apiUrl() + "/api/v1", "acme", @@ -127,12 +132,70 @@ public void testGiteaDispatchRequestsRunDetailsAndLatestRunUsesRepositoryRunsEnd assertThat(latest.get().runId()).isEqualTo(88); assertThat(server.requests()).contains( "/api/v1/repos/acme/tool/actions/workflows/build-update-chart.yaml/dispatches?return_run_details=true", - "/api/v1/repos/acme/tool/actions/runs?branch=main&event=workflow_dispatch&limit=1" + "/api/v1/repos/acme/tool/actions/runs?branch=main&event=workflow_dispatch&limit=20" ); assertThat(server.requests()).noneMatch(path -> path.contains("/workflows/build-update-chart.yaml/runs")); } } + public void testLatestRunPrefersSameGiteaWorkflowClosestToDispatchTime() throws Exception { + final List requests = new ArrayList<>(); + final WorkflowRun client = new WorkflowRun( + request -> { + requests.add(request.uri()); + return new ClientResponse(request, 200, "application/json", """ + {"workflow_runs":[ + {"id":99,"status":"queued","conclusion":null,"html_url":"wrong-newer","path":".gitea/workflows/other.yaml","created_at":"2026-06-15T10:00:03Z"}, + {"id":88,"status":"queued","conclusion":null,"html_url":"right","path":".gitea/workflows/build-update-chart.yaml","created_at":"2026-06-15T10:00:02Z"}, + {"id":77,"status":"queued","conclusion":null,"html_url":"right-old","path":".gitea/workflows/build-update-chart.yaml","created_at":"2026-06-15T09:00:00Z"} + ]} + """); + }, + request -> List.of(RemoteActionProviders.Authorizations.Authorization.anonymous()) + ); + final WorkflowRun.Request request = new WorkflowRun.Request( + "https://gitea.local/api/v1", + "acme", + "tool", + ".gitea/workflows/build-update-chart.yaml", + "main", + Map.of(), + "GITEA_TOKEN" + ); + + final Optional result = client.latestRun(request, Instant.parse("2026-06-15T10:00:00Z")); + + assertThat(result).isPresent(); + assertThat(result.get().runId()).isEqualTo(88); + assertThat(result.get().htmlUrl()).isEqualTo("right"); + assertThat(requests.get(0).getRawPath()).isEqualTo("/api/v1/repos/acme/tool/actions/runs"); + assertThat(requests.get(0).getRawQuery()).isEqualTo("branch=main&event=workflow_dispatch&limit=20"); + } + + public void testLatestRunReturnsEmptyWhenGiteaWorkflowPathDoesNotMatch() throws Exception { + final WorkflowRun client = new WorkflowRun( + request -> new ClientResponse(request, 200, "application/json", """ + {"workflow_runs":[ + {"id":99,"status":"queued","conclusion":null,"html_url":"wrong","path":".gitea/workflows/other.yaml","created_at":"2026-06-15T10:00:03Z"} + ]} + """), + request -> List.of(RemoteActionProviders.Authorizations.Authorization.anonymous()) + ); + final WorkflowRun.Request request = new WorkflowRun.Request( + "https://gitea.local/api/v1", + "acme", + "tool", + ".gitea/workflows/build-update-chart.yaml", + "main", + Map.of(), + "GITEA_TOKEN" + ); + + final Optional result = client.latestRun(request, Instant.parse("2026-06-15T10:00:00Z")); + + assertThat(result).isEmpty(); + } + public void testArtifactsAndZipUseRunArtifactEndpoints() throws Exception { try (FakeWorkflowRunServer server = new FakeWorkflowRunServer()) { final WorkflowRun client = new WorkflowRun(); @@ -286,6 +349,23 @@ public void testDispatchAuthenticationFailureMentionsGithubSettings() throws Exc } } + public void testGiteaDispatchAuthenticationFailureMentionsWorkflowSettings() throws Exception { + try (FakeWorkflowRunServer server = new FakeWorkflowRunServer(false, 1)) { + final HttpClient httpClient = HttpClient.newHttpClient(); + final WorkflowRun client = new WorkflowRun( + request -> httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)), + request -> List.of(RemoteActionProviders.Authorizations.Authorization.anonymous()) + ); + final WorkflowRun.Request request = new WorkflowRun.Request(server.apiUrl().replace("/api", "/api/v1"), "acme", "tool", ".gitea/workflows/build.yml", "main", Map.of(), ""); + + assertThatExceptionOfType(WorkflowRun.WorkflowRunHttpException.class) + .isThrownBy(() -> client.dispatch(request)) + .withMessageContaining("GitHub workflow dispatch failed with HTTP 401") + .withMessageContaining("Settings > Tools > GitHub Workflow") + .withMessageNotContaining("Settings > Version Control > GitHub"); + } + } + public void testJobLogHtmlFailureIsSummarized() { final WorkflowRun client = new WorkflowRun( request -> new ClientResponse( diff --git a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java index 2dbda2f..0ee29ad 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java @@ -381,6 +381,18 @@ public void testGiteaWarnsAboutUnsupportedExpressionFunctions() { """); } + public void testGiteaIgnoresExpressionFunctionNamesInsideStrings() { + assertGiteaWorkflowHighlights(""" + name: Syntax + on: workflow_dispatch + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo "${{ always() && 'startsWith(' != '' }}" + """); + } + public void testResolvedActionInputIsAccepted() { seedRemoteAction("owner/tool@v1", Map.of("known-input", "Known input"), Map.of()); From fe6c0923508af993040537560abb527c5586bd15 Mon Sep 17 00:00:00 2001 From: Yuna Morgenstern Date: Mon, 15 Jun 2026 10:13:51 +0200 Subject: [PATCH 5/8] Test Gitea workflow runner execution --- doc/spec/editor-test-matrix.md | 4 +- .../gitea-github-actions-compatibility.md | 4 +- .../git/GiteaDockerIntegrationTest.java | 166 +++++++++++++++++- 3 files changed, 169 insertions(+), 5 deletions(-) diff --git a/doc/spec/editor-test-matrix.md b/doc/spec/editor-test-matrix.md index df97535..f55a45b 100644 --- a/doc/spec/editor-test-matrix.md +++ b/doc/spec/editor-test-matrix.md @@ -22,7 +22,9 @@ Official syntax references: - Remote `uses` ref completion for tags/branches resolved from public GitHub and GitHub Enterprise-shaped servers through fake HTTP servers. - Gitea-compatible `/api/v1` remote metadata resolution is covered through fake HTTP tests and a default-on Docker smoke - test against the official rootless Gitea image on Unix-like hosts. Set `GITEA_DOCKER_TEST=false` to skip it. + test against the official rootless Gitea image. The same Docker suite also registers `act_runner`, dispatches a real + `.gitea/workflows` run through the plugin client, waits for completion, lists jobs, and downloads logs. Set + `GITEA_DOCKER_TEST=false` to skip it. - GitHub Enterprise servers registered in JetBrains GitHub settings are used as remote metadata sources; the plugin does not add a parallel server settings UI. - Cache actions and settings are registered through `plugin.xml`, localized through resource bundles, and covered by diff --git a/doc/spec/gitea-github-actions-compatibility.md b/doc/spec/gitea-github-actions-compatibility.md index 361913d..07288c3 100644 --- a/doc/spec/gitea-github-actions-compatibility.md +++ b/doc/spec/gitea-github-actions-compatibility.md @@ -51,6 +51,6 @@ shared behavior first and branch only where Gitea really differs. Tiny forks, no ## Test Shape - Fake HTTP tests cover provider inference, auth header scheme, dispatch URL, and run discovery URL. -- The Docker-backed Gitea integration test runs by default and seeds a tiny repository to verify `/api/v1` metadata - resolution for actions and `.gitea/workflows`. +- The Docker-backed Gitea integration test runs by default and seeds tiny repositories to verify `/api/v1` metadata + resolution plus a real `act_runner` workflow dispatch, run completion, job listing, and log download. - Keep Docker test opt-out explicit with `GITEA_DOCKER_TEST=false` for machines where Docker is unavailable. diff --git a/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java b/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java index 737cfb6..2ed1ad0 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java @@ -1,6 +1,7 @@ package com.github.yunabraska.githubworkflow.git; import com.github.yunabraska.githubworkflow.model.GitHubAction; +import com.github.yunabraska.githubworkflow.run.WorkflowRun; import com.google.gson.JsonObject; import com.intellij.testFramework.fixtures.BasePlatformTestCase; @@ -16,7 +17,9 @@ import java.time.Duration; import java.util.Base64; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; @@ -27,6 +30,9 @@ public class GiteaDockerIntegrationTest extends BasePlatformTestCase { private static final String IMAGE = Optional.ofNullable(System.getenv("GITEA_IMAGE")) .filter(value -> !value.isBlank()) .orElse("docker.gitea.com/gitea:1.26.2-rootless"); + private static final String RUNNER_IMAGE = Optional.ofNullable(System.getenv("GITEA_RUNNER_IMAGE")) + .filter(value -> !value.isBlank()) + .orElse("docker.io/gitea/act_runner:0.6.1"); private static final HttpClient CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); @@ -98,16 +104,104 @@ public void testGiteaApiV1ResolvesActionsAndWorkflowsFromEmbeddedContainer() thr } } + public void testGiteaWorkflowDispatchRunsJobAndDownloadsLogsWithActRunner() throws Exception { + if ("false".equalsIgnoreCase(System.getenv(TEST_SWITCH))) { + return; + } + try (GiteaContainer gitea = GiteaContainer.start()) { + final String token = gitea.createAdminToken(); + gitea.createRepository(token, "runner-smoke"); + gitea.createFile(token, "runner-smoke", ".gitea/workflows/smoke.yml", """ + name: Runner Smoke + on: + workflow_dispatch: + jobs: + smoke: + runs-on: ubuntu-latest + steps: + - name: Marker + shell: sh + run: echo plugin-gitea-runner-smoke + """); + try (RunnerContainer ignored = gitea.startRunner()) { + final RemoteActionProviders.Server server = RemoteActionProviders.Server.gitea( + "Embedded Gitea", + gitea.webUrl(), + gitea.apiUrl(), + "", + true + ); + RemoteActionProviders.Settings.getInstance() + .setCustomServers(List.of(server)) + .setGiteaToken(server, token); + final WorkflowRun workflowRun = new WorkflowRun(); + final WorkflowRun.Request request = new WorkflowRun.Request( + gitea.apiUrl(), + "test", + "runner-smoke", + ".gitea/workflows/smoke.yml", + "main", + Map.of(), + "GITEA_TOKEN" + ); + + final WorkflowRun.DispatchResult dispatch = workflowRun.dispatch(request); + final long runId = dispatch.runId() >= 0 + ? dispatch.runId() + : workflowRun.latestRun(request).orElseThrow().runId(); + final WorkflowRun.RunStatus completed = waitForCompletedRun(workflowRun, request, runId); + final List jobs = workflowRun.jobs(request, runId); + final String logs = workflowRun.jobLogs(request, jobs.getFirst().id()); + + assertThat(completed.conclusion()).isEqualTo("success"); + assertThat(jobs).hasSize(1); + assertThat(jobs.getFirst().name()).isEqualTo("smoke"); + assertThat(logs).contains("plugin-gitea-runner-smoke"); + } + } + } + + private static WorkflowRun.RunStatus waitForCompletedRun( + final WorkflowRun workflowRun, + final WorkflowRun.Request request, + final long runId + ) throws Exception { + for (int attempt = 0; attempt < 90; attempt++) { + final WorkflowRun.RunStatus status = workflowRun.status(request, runId); + if (status.completed()) { + return status; + } + Thread.sleep(1_000); + } + throw new IllegalStateException("Gitea workflow run did not complete"); + } + + private record RunnerContainer(String id) implements AutoCloseable { + + @Override + public void close() throws Exception { + run("docker", "rm", "-f", id); + } + } + private record GiteaContainer(String id, String webUrl) implements AutoCloseable { static GiteaContainer start() throws Exception { + final String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 12); + final String network = "gwplugin-" + suffix; + final String name = "gwplugin-gitea-" + suffix; + run("docker", "network", "create", network); final String id = run("docker", "run", "--rm", "-d", + "--name", name, + "--network", network, "-p", "127.0.0.1::3000", "-e", "GITEA__database__DB_TYPE=sqlite3", "-e", "GITEA__database__PATH=/var/lib/gitea/gitea.db", "-e", "GITEA__security__INSTALL_LOCK=true", "-e", "GITEA__service__DISABLE_REGISTRATION=true", "-e", "GITEA__server__HTTP_PORT=3000", + "-e", "GITEA__server__ROOT_URL=http://" + name + ":3000/", + "-e", "GITEA__actions__ENABLED=true", IMAGE ).trim(); final String port = run("docker", "port", id, "3000/tcp").trim().replaceFirst(".*:", ""); @@ -120,6 +214,35 @@ String apiUrl() { return webUrl + "/api/v1"; } + RunnerContainer startRunner() throws Exception { + final String runnerName = "gwplugin-runner-" + UUID.randomUUID().toString().replace("-", "").substring(0, 12); + final String runnerToken = run("docker", "exec", id, "gitea", "actions", "generate-runner-token") + .lines() + .reduce((first, second) -> second) + .orElse("") + .trim(); + if (runnerToken.isBlank()) { + throw new IllegalStateException("Gitea runner token was not printed"); + } + final String network = run("docker", "inspect", "-f", "{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{end}}", id).trim(); + run("docker", "run", "-d", + "--name", runnerName, + "--network", network, + "-e", "GITEA_INSTANCE_URL=http://" + containerName() + ":3000", + "-e", "GITEA_RUNNER_REGISTRATION_TOKEN=" + runnerToken, + "-e", "GITEA_RUNNER_NAME=plugin-smoke", + "-e", "GITEA_RUNNER_EPHEMERAL=1", + "-e", "GITEA_RUNNER_LABELS=ubuntu-latest:host", + RUNNER_IMAGE + ); + waitUntilRunnerReady(runnerName); + return new RunnerContainer(runnerName); + } + + private String containerName() throws Exception { + return run("docker", "inspect", "-f", "{{.Name}}", id).trim().replaceFirst("^/", ""); + } + String createAdminToken() throws Exception { run("docker", "exec", id, "gitea", "admin", "user", "create", "--username", "test", @@ -187,9 +310,28 @@ private void waitUntilReady() throws Exception { throw new IllegalStateException("Gitea did not become ready"); } + private static void waitUntilRunnerReady(final String runnerName) throws Exception { + final Pattern ready = Pattern.compile("(?i)(runner registered successfully|declare successfully)"); + for (int attempt = 0; attempt < 45; attempt++) { + final String logs = runCombined("docker", "logs", runnerName); + if (ready.matcher(logs).find()) { + return; + } + Thread.sleep(1_000); + } + throw new IllegalStateException("Gitea runner did not become ready: " + runCombined("docker", "logs", runnerName)); + } + @Override public void close() throws Exception { - run("docker", "stop", id); + final String network = run("docker", "inspect", "-f", "{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{end}}", id).trim(); + try { + run("docker", "stop", id); + } finally { + if (!network.isBlank()) { + run("docker", "network", "rm", network); + } + } } } @@ -204,7 +346,7 @@ private static String run(final String... command) throws IOException, Interrupt .redirectError(errorLog.toFile()) .start(); final String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - if (!process.waitFor(60, java.util.concurrent.TimeUnit.SECONDS)) { + if (!process.waitFor(180, java.util.concurrent.TimeUnit.SECONDS)) { process.destroyForcibly(); throw new IOException("Command timed out [" + String.join(" ", command) + "]: " + commandOutput(output, errorLog)); } @@ -217,6 +359,26 @@ private static String run(final String... command) throws IOException, Interrupt } } + private static String runCombined(final String... command) throws IOException, InterruptedException { + final Path errorLog = Files.createTempFile("gitea-docker-", ".err"); + try { + final Process process = new ProcessBuilder(command) + .redirectError(errorLog.toFile()) + .start(); + final String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + if (!process.waitFor(180, java.util.concurrent.TimeUnit.SECONDS)) { + process.destroyForcibly(); + throw new IOException("Command timed out [" + String.join(" ", command) + "]: " + commandOutput(output, errorLog)); + } + if (process.exitValue() != 0) { + throw new IOException("Command failed [" + String.join(" ", command) + "]: " + commandOutput(output, errorLog)); + } + return commandOutput(output, errorLog); + } finally { + Files.deleteIfExists(errorLog); + } + } + private static String commandOutput(final String output, final Path errorLog) throws IOException { final String error = Files.readString(errorLog, StandardCharsets.UTF_8); return (output + System.lineSeparator() + error).trim(); From 23cb619dc5e5145a6a7d66e9623ac0b9b7ad5564 Mon Sep 17 00:00:00 2001 From: Yuna Morgenstern Date: Mon, 15 Jun 2026 17:18:38 +0200 Subject: [PATCH 6/8] Harden Gitea settings integration --- .../entry/WorkflowCompletion.java | 2 +- .../git/RemoteActionProviders.java | 105 +++++- .../GitHubWorkflowSettingsConfigurable.java | 283 +++----------- .../settings/GiteaSettingsConfigurable.java | 350 ++++++++++++++++++ .../syntax/WorkflowContextCatalog.java | 10 +- src/main/resources/META-INF/plugin.xml | 6 + .../messages/GitHubWorkflowBundle.properties | 3 +- .../GitHubWorkflowBundle_ar.properties | 3 +- .../GitHubWorkflowBundle_cs.properties | 3 +- .../GitHubWorkflowBundle_de.properties | 3 +- .../GitHubWorkflowBundle_es.properties | 3 +- .../GitHubWorkflowBundle_fr.properties | 3 +- .../GitHubWorkflowBundle_hi.properties | 3 +- .../GitHubWorkflowBundle_id.properties | 3 +- .../GitHubWorkflowBundle_it.properties | 3 +- .../GitHubWorkflowBundle_ja.properties | 3 +- .../GitHubWorkflowBundle_ko.properties | 3 +- .../GitHubWorkflowBundle_nl.properties | 3 +- .../GitHubWorkflowBundle_pl.properties | 3 +- .../GitHubWorkflowBundle_pt_BR.properties | 3 +- .../GitHubWorkflowBundle_ru.properties | 3 +- .../GitHubWorkflowBundle_sv.properties | 3 +- .../GitHubWorkflowBundle_th.properties | 3 +- .../GitHubWorkflowBundle_tr.properties | 3 +- .../GitHubWorkflowBundle_uk.properties | 3 +- .../GitHubWorkflowBundle_vi.properties | 3 +- .../GitHubWorkflowBundle_zh_CN.properties | 3 +- .../entry/PluginWiringTest.java | 104 ++++++ .../git/RemoteActionProvidersTest.java | 20 + .../i18n/WorkflowMessagesTest.java | 2 + .../githubworkflow/run/WorkflowRunTest.java | 4 +- .../syntax/WorkflowMetadataTest.java | 14 +- 32 files changed, 700 insertions(+), 263 deletions(-) create mode 100644 src/main/java/com/github/yunabraska/githubworkflow/settings/GiteaSettingsConfigurable.java diff --git a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java index 981886c..1231974 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/entry/WorkflowCompletion.java @@ -242,7 +242,7 @@ private static boolean completeShell(final CompletionTrigger trigger) { } addLookupElements( trigger.resultSet().withPrefixMatcher(getDefaultPrefix(trigger.parameters())), - SHELLS, + shells(), NodeIcon.ICON_NODE, Character.MIN_VALUE ); diff --git a/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java b/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java index 2b32d49..9f7feae 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java @@ -10,6 +10,7 @@ import com.intellij.credentialStore.Credentials; import com.intellij.ide.passwordSafe.PasswordSafe; import com.intellij.ide.impl.ProjectUtil; +import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; @@ -39,7 +40,9 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.function.Predicate; @@ -396,6 +399,7 @@ public static class ServerState { public String apiUrl = ""; public String tokenEnvVar = ""; public boolean enabled = true; + public boolean tokenStored; public ServerState() { // XML serializer @@ -409,10 +413,11 @@ public ServerState() { apiUrl = normalized.apiUrl; tokenEnvVar = normalized.tokenEnvVar; enabled = normalized.enabled; + tokenStored = normalized.tokenStored; } Server server() { - return new Server(type, name, webUrl, apiUrl, tokenEnvVar, enabled).normalized(); + return new Server(type, name, webUrl, apiUrl, tokenEnvVar, enabled, tokenStored).normalized(); } } @@ -473,6 +478,7 @@ public Settings setGiteaToken(final Server server, final String token) { final Server normalized = Optional.ofNullable(server).orElseGet(Settings::defaultGitHub).normalized(); if (normalized.isGitea() && hasText(token)) { PasswordTokens.set(normalized, token); + markGiteaTokenStored(normalized, true); } return this; } @@ -487,6 +493,7 @@ public Settings clearGiteaToken(final Server server) { final Server normalized = Optional.ofNullable(server).orElseGet(Settings::defaultGitHub).normalized(); if (normalized.isGitea()) { PasswordTokens.clear(normalized); + markGiteaTokenStored(normalized, false); } return this; } @@ -509,7 +516,20 @@ public Optional giteaToken(final Server server) { * @return true when a token is stored */ public boolean hasGiteaToken(final Server server) { - return giteaToken(server).isPresent(); + final Server normalized = Optional.ofNullable(server).orElseGet(Settings::defaultGitHub).normalized(); + if (!normalized.isGitea()) { + return false; + } + return normalized.tokenStored || customServers().stream() + .map(Server::normalized) + .anyMatch(candidate -> candidate.key().equals(normalized.key()) && candidate.tokenStored); + } + + private Settings markGiteaTokenStored(final Server server, final boolean stored) { + state.servers.stream() + .filter(state -> state.server().key().equals(server.key())) + .forEach(state -> state.tokenStored = stored); + return this; } public List enabledServers() { @@ -569,6 +589,30 @@ private static Optional get(final Server server) { } private static void set(final Server server, final String token) { + write(() -> setNow(server, token)); + } + + private static void clear(final Server server) { + write(() -> clearNow(server)); + } + + private static void write(final Runnable operation) { + final Application application = ApplicationManager.getApplication(); + if (application != null && application.isDispatchThread()) { + try { + application.executeOnPooledThread(operation).get(); + } catch (final InterruptedException exception) { + Thread.currentThread().interrupt(); + LOG.warn("Gitea token write interrupted", exception); + } catch (final ExecutionException exception) { + LOG.warn("Gitea token write failed", exception); + } + return; + } + operation.run(); + } + + private static void setNow(final Server server, final String token) { try { PasswordSafe.getInstance().set(attributes(server), new Credentials(server.name, token)); } catch (final RuntimeException exception) { @@ -576,7 +620,7 @@ private static void set(final Server server, final String token) { } } - private static void clear(final Server server) { + private static void clearNow(final Server server) { try { PasswordSafe.getInstance().set(attributes(server), null); } catch (final RuntimeException exception) { @@ -597,6 +641,7 @@ public static class Server { public final String apiUrl; public final String tokenEnvVar; public final boolean enabled; + public final boolean tokenStored; public Server( final String name, @@ -615,6 +660,18 @@ public Server( final String apiUrl, final String tokenEnvVar, final boolean enabled + ) { + this(type, name, webUrl, apiUrl, tokenEnvVar, enabled, false); + } + + public Server( + final String type, + final String name, + final String webUrl, + final String apiUrl, + final String tokenEnvVar, + final boolean enabled, + final boolean tokenStored ) { this.type = Optional.ofNullable(type).map(String::trim).filter(RemoteActionProviders::hasText).orElse(Settings.TYPE_GITHUB); this.name = name; @@ -622,6 +679,7 @@ public Server( this.apiUrl = apiUrl; this.tokenEnvVar = tokenEnvVar; this.enabled = enabled; + this.tokenStored = tokenStored; } /** @@ -642,7 +700,18 @@ public static Server gitea( final String tokenEnvVar, final boolean enabled ) { - return new Server(Settings.TYPE_GITEA, name, webUrl, apiUrl, tokenEnvVar, enabled); + return gitea(name, webUrl, apiUrl, tokenEnvVar, enabled, false); + } + + public static Server gitea( + final String name, + final String webUrl, + final String apiUrl, + final String tokenEnvVar, + final boolean enabled, + final boolean tokenStored + ) { + return new Server(Settings.TYPE_GITEA, name, webUrl, apiUrl, tokenEnvVar, enabled, tokenStored); } /** @@ -701,10 +770,36 @@ public Server normalized() { trimTrailingSlash(webUrl), trimTrailingSlash(apiUrl), Optional.ofNullable(tokenEnvVar).map(String::trim).orElse(""), - enabled + enabled, + tokenStored ); } + @Override + public boolean equals(final Object object) { + if (this == object) { + return true; + } + if (!(object instanceof Server server)) { + return false; + } + final Server left = normalized(); + final Server right = server.normalized(); + return left.enabled == right.enabled + && left.tokenStored == right.tokenStored + && Objects.equals(left.type, right.type) + && Objects.equals(left.name, right.name) + && Objects.equals(left.webUrl, right.webUrl) + && Objects.equals(left.apiUrl, right.apiUrl) + && Objects.equals(left.tokenEnvVar, right.tokenEnvVar); + } + + @Override + public int hashCode() { + final Server normalized = normalized(); + return Objects.hash(normalized.type, normalized.name, normalized.webUrl, normalized.apiUrl, normalized.tokenEnvVar, normalized.enabled, normalized.tokenStored); + } + private String authorizationPrefix() { return isGitea() ? "token " : "Bearer "; } diff --git a/src/main/java/com/github/yunabraska/githubworkflow/settings/GitHubWorkflowSettingsConfigurable.java b/src/main/java/com/github/yunabraska/githubworkflow/settings/GitHubWorkflowSettingsConfigurable.java index 5427436..e97942a 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/settings/GitHubWorkflowSettingsConfigurable.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/settings/GitHubWorkflowSettingsConfigurable.java @@ -2,11 +2,9 @@ import com.github.yunabraska.githubworkflow.state.GitHubActionCache; -import com.github.yunabraska.githubworkflow.git.RemoteActionProviders; import com.github.yunabraska.githubworkflow.i18n.GitHubWorkflowBundle; import com.intellij.ide.BrowserUtil; -import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.options.SearchableConfigurable; import com.intellij.openapi.ui.Messages; import com.intellij.ui.table.JBTable; @@ -23,6 +21,7 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; +import javax.swing.border.TitledBorder; import javax.swing.table.DefaultTableModel; import java.awt.BorderLayout; import java.awt.FlowLayout; @@ -35,13 +34,9 @@ import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.concurrent.ThreadLocalRandom; /** @@ -50,14 +45,6 @@ public class GitHubWorkflowSettingsConfigurable implements SearchableConfigurable { private static final String SUPPORT_URL = "https://github.com/sponsors/YunaBraska"; - private static final String STORED_TOKEN = "********"; - private static final int GITEA_ENABLED = 0; - private static final int GITEA_NAME = 1; - private static final int GITEA_WEB_URL = 2; - private static final int GITEA_API_URL = 3; - private static final int GITEA_TOKEN_ENV = 4; - private static final int GITEA_TOKEN = 5; - private static final int GITEA_ROW_ID = 6; private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault()); private static final List LOCALES = List.of( new LocaleOption(GitHubWorkflowBundle.Settings.SYSTEM_LANGUAGE, "settings.language.system", true), @@ -84,28 +71,23 @@ public class GitHubWorkflowSettingsConfigurable implements SearchableConfigurabl ); private final GitHubWorkflowBundle.Settings settings = GitHubWorkflowBundle.Settings.getInstance(); - private final RemoteActionProviders.Settings remoteSettings = RemoteActionProviders.Settings.getInstance(); private final GitHubActionCache cache = GitHubActionCache.getActionCache(); private final JComboBox language = new JComboBox<>(LOCALES.toArray(LocaleOption[]::new)); - private final DefaultTableModel giteaModel = new DefaultTableModel() { - @Override - public Class getColumnClass(final int columnIndex) { - return columnIndex == GITEA_ENABLED ? Boolean.class : String.class; - } - - @Override - public boolean isCellEditable(final int row, final int column) { - return column != GITEA_TOKEN && column != GITEA_ROW_ID; - } - }; - private final JTable giteaTable = new JBTable(giteaModel); - private final Map pendingGiteaTokens = new HashMap<>(); - private final Set clearedGiteaTokens = new HashSet<>(); - private int nextGiteaRowSequence = 1; private final DefaultTableModel tableModel = new DefaultTableModel(); private final JTable table = new JBTable(tableModel); + private final JLabel languageLabel = new JLabel(); + private final JButton support = new JButton(); private final JLabel summary = new JLabel(); + private final List buttons = new ArrayList<>(); private @Nullable JPanel panel; + private @Nullable TitledBorder cacheBorder; + + /** + * Creates the settings page and wires the support button action. + */ + public GitHubWorkflowSettingsConfigurable() { + support.addActionListener(event -> BrowserUtil.browse(SUPPORT_URL)); + } @Override public @NotNull String getId() { @@ -119,13 +101,11 @@ public boolean isCellEditable(final int row, final int column) { @Override public @Nullable JComponent createComponent() { + buttons.clear(); panel = new JPanel(new BorderLayout(8, 8)); panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); panel.add(topPanel(), BorderLayout.NORTH); - final JPanel center = new JPanel(new BorderLayout(8, 8)); - center.add(giteaPanel(), BorderLayout.NORTH); - center.add(cachePanel(), BorderLayout.CENTER); - panel.add(center, BorderLayout.CENTER); + panel.add(cachePanel(), BorderLayout.CENTER); reset(); return panel; } @@ -133,18 +113,14 @@ public boolean isCellEditable(final int row, final int column) { @Override public boolean isModified() { final LocaleOption option = (LocaleOption) language.getSelectedItem(); - return option != null && !Objects.equals(option.tag(), settings.languageTag()) - || !Objects.equals(giteaServersFromTable(), remoteSettings.customServers().stream() - .filter(RemoteActionProviders.Server::isGitea) - .toList()) - || giteaTokenModified(); + return option != null && !Objects.equals(option.tag(), settings.languageTag()); } @Override - public void apply() throws ConfigurationException { + public void apply() { final LocaleOption option = (LocaleOption) language.getSelectedItem(); settings.languageTag(option == null ? GitHubWorkflowBundle.Settings.SYSTEM_LANGUAGE : option.tag()); - saveGiteaRows(); + refreshTexts(); reloadTable(); GitHubActionCache.triggerSyntaxHighlightingForActiveFiles(); } @@ -152,7 +128,7 @@ public void apply() throws ConfigurationException { @Override public void reset() { selectLanguage(settings.languageTag()); - reloadGiteaTable(); + refreshTexts(); reloadTable(); } @@ -168,7 +144,7 @@ private JPanel topPanel() { label.gridy = 0; label.anchor = GridBagConstraints.WEST; label.insets = new Insets(0, 0, 0, 8); - result.add(new JLabel(GitHubWorkflowBundle.message("settings.language.label")), label); + result.add(languageLabel, label); final GridBagConstraints combo = new GridBagConstraints(); combo.gridx = 1; @@ -177,9 +153,6 @@ private JPanel topPanel() { combo.fill = GridBagConstraints.HORIZONTAL; result.add(language, combo); - final JButton support = new JButton(randomSupportLine()); - support.setToolTipText(GitHubWorkflowBundle.message("settings.support.tooltip")); - support.addActionListener(event -> BrowserUtil.browse(SUPPORT_URL)); final GridBagConstraints supportConstraints = new GridBagConstraints(); supportConstraints.gridx = 2; supportConstraints.gridy = 0; @@ -188,49 +161,13 @@ private JPanel topPanel() { return result; } - private JPanel giteaPanel() { - giteaModel.setColumnIdentifiers(new Object[]{ - GitHubWorkflowBundle.message("settings.gitea.column.enabled"), - GitHubWorkflowBundle.message("settings.gitea.column.name"), - GitHubWorkflowBundle.message("settings.gitea.column.webUrl"), - GitHubWorkflowBundle.message("settings.gitea.column.apiUrl"), - GitHubWorkflowBundle.message("settings.gitea.column.tokenEnv"), - GitHubWorkflowBundle.message("settings.gitea.column.token"), - "" - }); - if (giteaTable.getColumnModel().getColumnCount() > GITEA_ROW_ID) { - giteaTable.removeColumn(giteaTable.getColumnModel().getColumn(GITEA_ROW_ID)); - } - giteaTable.setSelectionMode(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - - final JPanel result = new JPanel(new BorderLayout(4, 4)); - result.setBorder(BorderFactory.createTitledBorder(GitHubWorkflowBundle.message("settings.gitea.title"))); - result.add(new JScrollPane(giteaTable), BorderLayout.CENTER); - result.add(giteaButtons(), BorderLayout.SOUTH); - return result; - } - - private JPanel giteaButtons() { - final JPanel result = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); - addButton(result, "settings.gitea.add", this::addGiteaRow); - addButton(result, "settings.gitea.remove", this::removeSelectedGiteaRows); - addButton(result, "settings.gitea.setToken", this::setSelectedGiteaToken); - addButton(result, "settings.gitea.clearToken", this::clearSelectedGiteaToken); - return result; - } - private JPanel cachePanel() { - tableModel.setColumnIdentifiers(new Object[]{ - GitHubWorkflowBundle.message("settings.cache.column.key"), - GitHubWorkflowBundle.message("settings.cache.column.name"), - GitHubWorkflowBundle.message("settings.cache.column.kind"), - GitHubWorkflowBundle.message("settings.cache.column.state"), - GitHubWorkflowBundle.message("settings.cache.column.expires") - }); + setCacheColumnHeaders(); table.setSelectionMode(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); final JPanel result = new JPanel(new BorderLayout(4, 4)); - result.setBorder(BorderFactory.createTitledBorder(GitHubWorkflowBundle.message("settings.cache.title"))); + cacheBorder = BorderFactory.createTitledBorder(GitHubWorkflowBundle.message("settings.cache.title")); + result.setBorder(cacheBorder); result.add(summary, BorderLayout.NORTH); result.add(new JScrollPane(table), BorderLayout.CENTER); result.add(cacheButtons(), BorderLayout.SOUTH); @@ -250,156 +187,10 @@ private JPanel cacheButtons() { private void addButton(final JPanel panel, final String key, final Runnable action) { final JButton button = new JButton(GitHubWorkflowBundle.message(key)); button.addActionListener(event -> action.run()); + buttons.add(new LocalizedButton(button, key)); panel.add(button); } - private void reloadGiteaTable() { - pendingGiteaTokens.clear(); - clearedGiteaTokens.clear(); - giteaModel.setRowCount(0); - remoteSettings.customServers().stream() - .filter(RemoteActionProviders.Server::isGitea) - .forEach(server -> giteaModel.addRow(new Object[]{ - server.enabled, - server.name, - server.webUrl, - server.apiUrl, - server.tokenEnvVar, - remoteSettings.hasGiteaToken(server) ? STORED_TOKEN : "", - nextGiteaRowId() - })); - } - - private void addGiteaRow() { - giteaModel.addRow(new Object[]{ - true, - "Gitea", - "https://gitea.com", - "https://gitea.com/api/v1", - "GITEA_TOKEN", - "", - nextGiteaRowId() - }); - } - - private void removeSelectedGiteaRows() { - final int[] rows = giteaTable.getSelectedRows(); - for (int index = rows.length - 1; index >= 0; index--) { - giteaModel.removeRow(giteaTable.convertRowIndexToModel(rows[index])); - } - } - - private void setSelectedGiteaToken() { - final int row = selectedGiteaRow(); - if (row < 0) { - Messages.showInfoMessage(panel, GitHubWorkflowBundle.message("settings.gitea.noneSelected"), getDisplayName()); - return; - } - final String token = Messages.showPasswordDialog(GitHubWorkflowBundle.message("settings.gitea.token.prompt"), getDisplayName()); - if (token != null && !token.isBlank()) { - final int rowId = rowId(row); - pendingGiteaTokens.put(rowId, token); - clearedGiteaTokens.remove(rowId); - giteaModel.setValueAt(STORED_TOKEN, row, GITEA_TOKEN); - } - } - - private void clearSelectedGiteaToken() { - final int row = selectedGiteaRow(); - if (row < 0) { - Messages.showInfoMessage(panel, GitHubWorkflowBundle.message("settings.gitea.noneSelected"), getDisplayName()); - return; - } - final int rowId = rowId(row); - pendingGiteaTokens.remove(rowId); - clearedGiteaTokens.add(rowId); - giteaModel.setValueAt("", row, GITEA_TOKEN); - } - - private int selectedGiteaRow() { - final int viewRow = giteaTable.getSelectedRow(); - return viewRow < 0 ? -1 : giteaTable.convertRowIndexToModel(viewRow); - } - - private void saveGiteaRows() { - final List previous = remoteSettings.customServers(); - final List servers = giteaServersFromTable(); - final List nextKeys = servers.stream().map(server -> server.apiUrl).toList(); - final List persisted = new ArrayList<>(previous.stream() - .filter(server -> !server.isGitea()) - .toList()); - persisted.addAll(servers); - - previous.stream() - .filter(RemoteActionProviders.Server::isGitea) - .filter(server -> !nextKeys.contains(server.apiUrl)) - .forEach(remoteSettings::clearGiteaToken); - - for (int row = 0; row < giteaModel.getRowCount(); row++) { - final RemoteActionProviders.Server server = giteaServerFromRow(row); - if (!server.isValid()) { - continue; - } - final int rowId = rowId(row); - if (pendingGiteaTokens.containsKey(rowId)) { - remoteSettings.setGiteaToken(server, pendingGiteaTokens.get(rowId)); - } else if (clearedGiteaTokens.contains(rowId)) { - remoteSettings.clearGiteaToken(server); - } - } - remoteSettings.setCustomServers(persisted); - reloadGiteaTable(); - } - - private boolean giteaTokenModified() { - if (!pendingGiteaTokens.isEmpty()) { - return true; - } - for (int row = 0; row < giteaModel.getRowCount(); row++) { - final RemoteActionProviders.Server server = giteaServerFromRow(row); - if (clearedGiteaTokens.contains(rowId(row)) && remoteSettings.hasGiteaToken(server)) { - return true; - } - } - return false; - } - - private List giteaServersFromTable() { - final List result = new ArrayList<>(); - for (int row = 0; row < giteaModel.getRowCount(); row++) { - final RemoteActionProviders.Server server = giteaServerFromRow(row); - if (server.isValid()) { - result.add(server); - } - } - return result; - } - - private RemoteActionProviders.Server giteaServerFromRow(final int row) { - return RemoteActionProviders.Server.gitea( - text(row, GITEA_NAME), - text(row, GITEA_WEB_URL), - text(row, GITEA_API_URL), - text(row, GITEA_TOKEN_ENV), - Boolean.TRUE.equals(giteaModel.getValueAt(row, GITEA_ENABLED)) - ).normalized(); - } - - private String text(final int row, final int column) { - return Objects.toString(giteaModel.getValueAt(row, column), "").trim(); - } - - private int rowId(final int row) { - final Object value = giteaModel.getValueAt(row, GITEA_ROW_ID); - return value instanceof Integer id ? id : -1; - } - - private int nextGiteaRowId() { - final int next = nextGiteaRowSequence; - nextGiteaRowSequence++; - return next; - } - private void reloadTable() { tableModel.setRowCount(0); cache.entries().forEach(entry -> tableModel.addRow(new Object[]{ @@ -423,6 +214,31 @@ private void reloadTable() { summary.setToolTipText(null); } + private void setCacheColumnHeaders() { + tableModel.setColumnIdentifiers(new Object[]{ + GitHubWorkflowBundle.message("settings.cache.column.key"), + GitHubWorkflowBundle.message("settings.cache.column.name"), + GitHubWorkflowBundle.message("settings.cache.column.kind"), + GitHubWorkflowBundle.message("settings.cache.column.state"), + GitHubWorkflowBundle.message("settings.cache.column.expires") + }); + } + + private void refreshTexts() { + languageLabel.setText(GitHubWorkflowBundle.message("settings.language.label")); + support.setText(randomSupportLine()); + support.setToolTipText(GitHubWorkflowBundle.message("settings.support.tooltip")); + buttons.forEach(button -> button.component().setText(GitHubWorkflowBundle.message(button.key()))); + setCacheColumnHeaders(); + if (cacheBorder != null) { + cacheBorder.setTitle(GitHubWorkflowBundle.message("settings.cache.title")); + } + if (panel != null) { + panel.revalidate(); + panel.repaint(); + } + } + private String stateText(final GitHubActionCache.CacheEntry entry) { if (entry.suppressed()) { return GitHubWorkflowBundle.message("settings.cache.state.suppressed"); @@ -513,4 +329,7 @@ public String toString() { return bundleKey ? GitHubWorkflowBundle.message(label) : label; } } + + private record LocalizedButton(JButton component, String key) { + } } diff --git a/src/main/java/com/github/yunabraska/githubworkflow/settings/GiteaSettingsConfigurable.java b/src/main/java/com/github/yunabraska/githubworkflow/settings/GiteaSettingsConfigurable.java new file mode 100644 index 0000000..ac88864 --- /dev/null +++ b/src/main/java/com/github/yunabraska/githubworkflow/settings/GiteaSettingsConfigurable.java @@ -0,0 +1,350 @@ +package com.github.yunabraska.githubworkflow.settings; + +import com.github.yunabraska.githubworkflow.git.RemoteActionProviders; +import com.github.yunabraska.githubworkflow.i18n.GitHubWorkflowBundle; +import com.github.yunabraska.githubworkflow.state.GitHubActionCache; + +import com.intellij.openapi.options.SearchableConfigurable; +import com.intellij.openapi.ui.Messages; +import com.intellij.ui.table.JBTable; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.border.TitledBorder; +import javax.swing.table.DefaultTableModel; +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Settings UI for Gitea accounts used by workflow metadata and run requests. + */ +public class GiteaSettingsConfigurable implements SearchableConfigurable { + + private static final String STORED_TOKEN = "********"; + private static final int ENABLED = 0; + private static final int NAME = 1; + private static final int WEB_URL = 2; + private static final int API_URL = 3; + private static final int TOKEN_ENV = 4; + private static final int TOKEN = 5; + private static final int ROW_ID = 6; + + private final RemoteActionProviders.Settings settings = RemoteActionProviders.Settings.getInstance(); + private final DefaultTableModel model = new DefaultTableModel() { + @Override + public Class getColumnClass(final int columnIndex) { + return columnIndex == ENABLED ? Boolean.class : String.class; + } + + @Override + public boolean isCellEditable(final int row, final int column) { + return column != TOKEN && column != ROW_ID; + } + }; + private final JTable table = new JBTable(model); + private final Map pendingTokens = new HashMap<>(); + private final Set clearedTokens = new HashSet<>(); + private final Set storedTokenRows = new HashSet<>(); + private final List buttons = new ArrayList<>(); + private int nextRowSequence = 1; + private @Nullable JPanel panel; + private @Nullable TitledBorder border; + + /** + * Returns the stable settings identifier used by IntelliJ Settings search. + * + * @return the Gitea settings configurable id + */ + @Override + public @NotNull String getId() { + return "github.workflow.gitea.settings"; + } + + /** + * Returns the localized Gitea settings page title. + * + * @return localized display name + */ + @Override + public @Nls String getDisplayName() { + return GitHubWorkflowBundle.message("settings.gitea.displayName"); + } + + /** + * Builds the Gitea account editor. + * + * @return settings component + */ + @Override + public @Nullable JComponent createComponent() { + buttons.clear(); + panel = new JPanel(new BorderLayout(4, 4)); + panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + panel.add(accountsPanel(), BorderLayout.CENTER); + reset(); + return panel; + } + + /** + * Reports whether table rows or pending token edits differ from persisted settings. + * + * @return {@code true} when user edits are pending + */ + @Override + public boolean isModified() { + return !Objects.equals(serversFromTable(), settings.customServers().stream() + .filter(RemoteActionProviders.Server::isGitea) + .toList()) + || tokenModified(); + } + + /** + * Stores Gitea account rows and tokens in IDE settings and Password Safe. + */ + @Override + public void apply() { + saveRows(); + refreshTexts(); + GitHubActionCache.triggerSyntaxHighlightingForActiveFiles(); + } + + /** + * Reloads persisted Gitea account rows and refreshes localized labels. + */ + @Override + public void reset() { + reloadTable(); + refreshTexts(); + } + + /** + * Releases Swing component references. + */ + @Override + public void disposeUIResources() { + panel = null; + border = null; + } + + private JPanel accountsPanel() { + setColumnHeaders(); + table.setSelectionMode(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + + final JPanel result = new JPanel(new BorderLayout(4, 4)); + border = BorderFactory.createTitledBorder(GitHubWorkflowBundle.message("settings.gitea.title")); + result.setBorder(border); + result.add(new JScrollPane(table), BorderLayout.CENTER); + result.add(buttonsPanel(), BorderLayout.SOUTH); + return result; + } + + private JPanel buttonsPanel() { + final JPanel result = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); + addButton(result, "settings.gitea.add", this::addRow); + addButton(result, "settings.gitea.remove", this::removeSelectedRows); + addButton(result, "settings.gitea.setToken", this::setSelectedToken); + addButton(result, "settings.gitea.clearToken", this::clearSelectedToken); + return result; + } + + private void addButton(final JPanel panel, final String key, final Runnable action) { + final JButton button = new JButton(GitHubWorkflowBundle.message(key)); + button.addActionListener(event -> action.run()); + buttons.add(new LocalizedButton(button, key)); + panel.add(button); + } + + private void reloadTable() { + pendingTokens.clear(); + clearedTokens.clear(); + storedTokenRows.clear(); + model.setRowCount(0); + settings.customServers().stream() + .filter(RemoteActionProviders.Server::isGitea) + .forEach(this::addPersistedRow); + } + + private void addPersistedRow(final RemoteActionProviders.Server server) { + final int rowId = nextRowId(); + if (server.tokenStored) { + storedTokenRows.add(rowId); + } + model.addRow(new Object[]{ + server.enabled, + server.name, + server.webUrl, + server.apiUrl, + server.tokenEnvVar, + server.tokenStored ? STORED_TOKEN : "", + rowId + }); + } + + private void addRow() { + model.addRow(new Object[]{ + true, + "Gitea", + "https://gitea.com", + "https://gitea.com/api/v1", + "GITEA_TOKEN", + "", + nextRowId() + }); + } + + private void removeSelectedRows() { + final int[] rows = table.getSelectedRows(); + for (int index = rows.length - 1; index >= 0; index--) { + model.removeRow(table.convertRowIndexToModel(rows[index])); + } + } + + private void setSelectedToken() { + final int row = selectedRow(); + if (row < 0) { + Messages.showInfoMessage(panel, GitHubWorkflowBundle.message("settings.gitea.noneSelected"), getDisplayName()); + return; + } + final String token = Messages.showPasswordDialog(GitHubWorkflowBundle.message("settings.gitea.token.prompt"), getDisplayName()); + if (token != null && !token.isBlank()) { + final int rowId = rowId(row); + pendingTokens.put(rowId, token); + clearedTokens.remove(rowId); + storedTokenRows.add(rowId); + model.setValueAt(STORED_TOKEN, row, TOKEN); + } + } + + private void clearSelectedToken() { + final int row = selectedRow(); + if (row < 0) { + Messages.showInfoMessage(panel, GitHubWorkflowBundle.message("settings.gitea.noneSelected"), getDisplayName()); + return; + } + final int rowId = rowId(row); + pendingTokens.remove(rowId); + clearedTokens.add(rowId); + storedTokenRows.remove(rowId); + model.setValueAt("", row, TOKEN); + } + + private int selectedRow() { + final int viewRow = table.getSelectedRow(); + return viewRow < 0 ? -1 : table.convertRowIndexToModel(viewRow); + } + + private void saveRows() { + final List previous = settings.customServers(); + final List servers = serversFromTable(); + final List nextKeys = servers.stream().map(server -> server.apiUrl).toList(); + final List persisted = new ArrayList<>(previous.stream() + .filter(server -> !server.isGitea()) + .toList()); + persisted.addAll(servers); + + previous.stream() + .filter(RemoteActionProviders.Server::isGitea) + .filter(server -> !nextKeys.contains(server.apiUrl)) + .forEach(settings::clearGiteaToken); + + for (int row = 0; row < model.getRowCount(); row++) { + final RemoteActionProviders.Server server = serverFromRow(row); + if (!server.isValid()) { + continue; + } + final int rowId = rowId(row); + if (pendingTokens.containsKey(rowId)) { + settings.setGiteaToken(server, pendingTokens.get(rowId)); + } else if (clearedTokens.contains(rowId)) { + settings.clearGiteaToken(server); + } + } + settings.setCustomServers(persisted); + reloadTable(); + } + + private boolean tokenModified() { + return !pendingTokens.isEmpty(); + } + + private List serversFromTable() { + final List result = new ArrayList<>(); + for (int row = 0; row < model.getRowCount(); row++) { + final RemoteActionProviders.Server server = serverFromRow(row); + if (server.isValid()) { + result.add(server); + } + } + return result; + } + + private RemoteActionProviders.Server serverFromRow(final int row) { + return RemoteActionProviders.Server.gitea( + text(row, NAME), + text(row, WEB_URL), + text(row, API_URL), + text(row, TOKEN_ENV), + Boolean.TRUE.equals(model.getValueAt(row, ENABLED)), + storedTokenRows.contains(rowId(row)) + ).normalized(); + } + + private String text(final int row, final int column) { + return Objects.toString(model.getValueAt(row, column), "").trim(); + } + + private int rowId(final int row) { + final Object value = model.getValueAt(row, ROW_ID); + return value instanceof Integer id ? id : -1; + } + + private int nextRowId() { + final int next = nextRowSequence; + nextRowSequence++; + return next; + } + + private void refreshTexts() { + buttons.forEach(button -> button.component().setText(GitHubWorkflowBundle.message(button.key()))); + setColumnHeaders(); + if (border != null) { + border.setTitle(GitHubWorkflowBundle.message("settings.gitea.title")); + } + if (panel != null) { + panel.revalidate(); + panel.repaint(); + } + } + + private void setColumnHeaders() { + model.setColumnIdentifiers(new Object[]{ + GitHubWorkflowBundle.message("settings.gitea.column.enabled"), + GitHubWorkflowBundle.message("settings.gitea.column.name"), + GitHubWorkflowBundle.message("settings.gitea.column.webUrl"), + GitHubWorkflowBundle.message("settings.gitea.column.apiUrl"), + GitHubWorkflowBundle.message("settings.gitea.column.tokenEnv"), + GitHubWorkflowBundle.message("settings.gitea.column.token"), + "" + }); + if (table.getColumnModel().getColumnCount() > ROW_ID) { + table.removeColumn(table.getColumnModel().getColumn(ROW_ID)); + } + } + + private record LocalizedButton(JButton component, String key) { + } +} diff --git a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java index 899fd70..3659a8b 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java @@ -54,7 +54,6 @@ public class WorkflowContextCatalog { public static final String FIELD_CONCLUSION = "conclusion"; public static final String FIELD_OUTCOME = "outcome"; public static final Map>> DEFAULT_VALUE_MAP = initProcessorMap(); - public static final Map SHELLS = initShells(); private static final List GITEA_ENV_NAMES = List.of( "GITEA_ACTIONS", "GITEA_ACTIONS_RUNNER_VERSION", @@ -129,6 +128,15 @@ public static List githubExpressionFunctionNames() { .toList(); } + /** + * Returns supported shell completion values with localized descriptions. + * + * @return immutable map keyed by shell name + */ + public static Map shells() { + return initShells(); + } + private static Map initShells() { final Map result = new LinkedHashMap<>(); result.put("bash", message("completion.shell.bash")); diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 9f76a32..9a551b6 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -55,6 +55,12 @@ parentId="tools" key="settings.displayName" bundle="messages.GitHubWorkflowBundle"/> + diff --git a/src/main/resources/messages/GitHubWorkflowBundle.properties b/src/main/resources/messages/GitHubWorkflowBundle.properties index 1008785..23207f5 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Stop workflow run workflow.run.gutter.stop.text=Stop workflow run workflow.run.gutter.stop.description=Cancel this run workflow.run.auth.settings.github=Settings > Version Control > GitHub -workflow.run.auth.settings.gitea=Settings > Tools > GitHub Workflow +workflow.run.auth.settings.gitea=Settings > Version Control > Gitea workflow.run.auth.add=Add a token or account in {0}. workflow.log.command=run: workflow.log.warning=warning: @@ -384,6 +384,7 @@ completion.steps.outcome=The result of a completed step before continue-on-error completion.jobs.outputs=The set of outputs defined for the job. completion.jobs.result=The result of the job. settings.displayName=GitHub Workflow +settings.gitea.displayName=Gitea settings.language.label=Language: settings.language.system=IDE/system default settings.gitea.title=Gitea accounts diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ar.properties b/src/main/resources/messages/GitHubWorkflowBundle_ar.properties index 084e3cf..cdfa070 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ar.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ar.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=إيقاف تشغيل سير العمل workflow.run.gutter.stop.text=إيقاف تشغيل سير العمل workflow.run.gutter.stop.description=إلغاء هذا التشغيل workflow.run.auth.settings.github=الإعدادات > التحكم بالإصدار > GitHub -workflow.run.auth.settings.gitea=الإعدادات > الأدوات > GitHub Workflow +workflow.run.auth.settings.gitea=الإعدادات > التحكم بالإصدار > Gitea workflow.run.auth.add=أضف رمزا أو حسابا في {0}. workflow.log.command=يجري: workflow.log.warning=تحذير: @@ -384,6 +384,7 @@ completion.steps.outcome=يتم تطبيق نتيجة الخطوة المكتم completion.jobs.outputs=مجموعة المخرجات المحددة للوظيفة. completion.jobs.result=نتيجة الوظيفة. settings.displayName=سير العمل GitHub +settings.gitea.displayName=Gitea settings.language.label=لغة: settings.language.system=IDE/النظام الافتراضي settings.gitea.title=حسابات Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_cs.properties b/src/main/resources/messages/GitHubWorkflowBundle_cs.properties index e02167f..3185f1b 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_cs.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_cs.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Zastavit běh pracovního postupu workflow.run.gutter.stop.text=Zastavit běh pracovního postupu workflow.run.gutter.stop.description=Zrušte tento běh workflow.run.auth.settings.github=Nastavení > Správa verzí > GitHub -workflow.run.auth.settings.gitea=Nastavení > Nástroje > GitHub Workflow +workflow.run.auth.settings.gitea=Nastavení > Správa verzí > Gitea workflow.run.auth.add=Přidej token nebo účet v {0}. workflow.log.command=spustit: workflow.log.warning=varování: @@ -384,6 +384,7 @@ completion.steps.outcome=Použije se výsledek dokončeného kroku před pokrač completion.jobs.outputs=Sada výstupů definovaných pro úlohu. completion.jobs.result=Výsledek práce. settings.displayName=Pracovní postup GitHub +settings.gitea.displayName=Gitea settings.language.label=jazyk: settings.language.system=IDE/výchozí nastavení systému settings.gitea.title=Účty Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_de.properties b/src/main/resources/messages/GitHubWorkflowBundle_de.properties index 94f24f1..d0aad10 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_de.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_de.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Stoppen Sie die Workflow-Ausführung workflow.run.gutter.stop.text=Stoppen Sie die Workflow-Ausführung workflow.run.gutter.stop.description=Brechen Sie diesen Lauf ab workflow.run.auth.settings.github=Einstellungen > Versionskontrolle > GitHub -workflow.run.auth.settings.gitea=Einstellungen > Tools > GitHub Workflow +workflow.run.auth.settings.gitea=Einstellungen > Versionskontrolle > Gitea workflow.run.auth.add=Token oder Konto in {0} hinzufügen. workflow.log.command=Lauf: workflow.log.warning=Warnung: @@ -384,6 +384,7 @@ completion.steps.outcome=Das Ergebnis eines abgeschlossenen Schritts, bevor „C completion.jobs.outputs=Der für den Job definierte Satz von Ausgaben. completion.jobs.result=Das Ergebnis der Arbeit. settings.displayName=GitHub-Workflow +settings.gitea.displayName=Gitea settings.language.label=Sprache: settings.language.system=IDE/Systemstandard settings.gitea.title=Gitea-Konten diff --git a/src/main/resources/messages/GitHubWorkflowBundle_es.properties b/src/main/resources/messages/GitHubWorkflowBundle_es.properties index ec2b66c..0ec24cb 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_es.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_es.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Detener la ejecución del flujo de trabajo workflow.run.gutter.stop.text=Detener la ejecución del flujo de trabajo workflow.run.gutter.stop.description=Cancelar esta ejecución workflow.run.auth.settings.github=Ajustes > Control de versiones > GitHub -workflow.run.auth.settings.gitea=Ajustes > Herramientas > GitHub Workflow +workflow.run.auth.settings.gitea=Ajustes > Control de versiones > Gitea workflow.run.auth.add=Añade un token o cuenta en {0}. workflow.log.command=ejecutar: workflow.log.warning=advertencia: @@ -384,6 +384,7 @@ completion.steps.outcome=Se aplica el resultado de un paso completado antes de c completion.jobs.outputs=El conjunto de salidas definidas para el trabajo. completion.jobs.result=El resultado del trabajo. settings.displayName=Flujo de trabajo GitHub +settings.gitea.displayName=Gitea settings.language.label=Idioma: settings.language.system=IDE/valor predeterminado del sistema settings.gitea.title=Cuentas de Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_fr.properties b/src/main/resources/messages/GitHubWorkflowBundle_fr.properties index 4c48367..8f4596d 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_fr.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_fr.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Arrêter l''exécution du workflow workflow.run.gutter.stop.text=Arrêter l''exécution du workflow workflow.run.gutter.stop.description=Annuler cette course workflow.run.auth.settings.github=Paramètres > Contrôle de version > GitHub -workflow.run.auth.settings.gitea=Paramètres > Outils > GitHub Workflow +workflow.run.auth.settings.gitea=Paramètres > Contrôle de version > Gitea workflow.run.auth.add=Ajoutez un jeton ou un compte dans {0}. workflow.log.command=exécuter : workflow.log.warning=avertissement : @@ -384,6 +384,7 @@ completion.steps.outcome=Le résultat d’une étape terminée avant l’applica completion.jobs.outputs=L''ensemble des sorties définies pour le travail. completion.jobs.result=Le résultat du travail. settings.displayName=Flux de travail GitHub +settings.gitea.displayName=Gitea settings.language.label=Langue : settings.language.system=IDE/système par défaut settings.gitea.title=Comptes Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_hi.properties b/src/main/resources/messages/GitHubWorkflowBundle_hi.properties index abc8e28..742e868 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_hi.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_hi.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=वर्कफ़्लो चलाना बंद workflow.run.gutter.stop.text=वर्कफ़्लो चलाना बंद करें workflow.run.gutter.stop.description=यह रन रद्द करें workflow.run.auth.settings.github=सेटिंग्स > वर्शन कंट्रोल > GitHub -workflow.run.auth.settings.gitea=सेटिंग्स > टूल्स > GitHub Workflow +workflow.run.auth.settings.gitea=सेटिंग्स > वर्शन कंट्रोल > Gitea workflow.run.auth.add={0} में टोकन या खाता जोड़ें. workflow.log.command=चलाएँ: workflow.log.warning=चेतावनी: @@ -384,6 +384,7 @@ completion.steps.outcome=जारी रखें-त्रुटि लाग completion.jobs.outputs=कार्य के लिए परिभाषित आउटपुट का सेट। completion.jobs.result=कार्य का परिणाम. settings.displayName=GitHub वर्कफ़्लो +settings.gitea.displayName=Gitea settings.language.label=भाषा: settings.language.system=IDE/सिस्टम डिफ़ॉल्ट settings.gitea.title=Gitea खाते diff --git a/src/main/resources/messages/GitHubWorkflowBundle_id.properties b/src/main/resources/messages/GitHubWorkflowBundle_id.properties index 1500a3b..8cb84bd 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_id.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_id.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Hentikan alur kerja yang dijalankan workflow.run.gutter.stop.text=Hentikan alur kerja yang dijalankan workflow.run.gutter.stop.description=Batalkan proses ini workflow.run.auth.settings.github=Pengaturan > Kontrol Versi > GitHub -workflow.run.auth.settings.gitea=Pengaturan > Alat > GitHub Workflow +workflow.run.auth.settings.gitea=Pengaturan > Kontrol Versi > Gitea workflow.run.auth.add=Tambahkan token atau akun di {0}. workflow.log.command=menjalankan: workflow.log.warning=peringatan: @@ -384,6 +384,7 @@ completion.steps.outcome=Hasil dari langkah yang diselesaikan sebelum kesalahan completion.jobs.outputs=Kumpulan output yang ditentukan untuk pekerjaan itu. completion.jobs.result=Hasil pekerjaan. settings.displayName=Alur Kerja GitHub +settings.gitea.displayName=Gitea settings.language.label=Bahasa: settings.language.system=IDE/default sistem settings.gitea.title=Akun Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_it.properties b/src/main/resources/messages/GitHubWorkflowBundle_it.properties index 8642cb9..f096812 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_it.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_it.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Arresta l''esecuzione del flusso di lavoro workflow.run.gutter.stop.text=Arresta l''esecuzione del flusso di lavoro workflow.run.gutter.stop.description=Annulla questa corsa workflow.run.auth.settings.github=Impostazioni > Controllo versione > GitHub -workflow.run.auth.settings.gitea=Impostazioni > Strumenti > GitHub Workflow +workflow.run.auth.settings.gitea=Impostazioni > Controllo versione > Gitea workflow.run.auth.add=Aggiungi token o account in {0}. workflow.log.command=correre: workflow.log.warning=avvertimento: @@ -384,6 +384,7 @@ completion.steps.outcome=Il risultato di un passaggio completato prima che venga completion.jobs.outputs=L''insieme di output definiti per il lavoro. completion.jobs.result=Il risultato del lavoro. settings.displayName=Flusso di lavoro GitHub +settings.gitea.displayName=Gitea settings.language.label=Lingua: settings.language.system=IDE/impostazione predefinita del sistema settings.gitea.title=Account Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ja.properties b/src/main/resources/messages/GitHubWorkflowBundle_ja.properties index 6495103..b9c2337 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ja.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ja.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=ワークフローの実行を停止する workflow.run.gutter.stop.text=ワークフローの実行を停止する workflow.run.gutter.stop.description=この実行をキャンセルする workflow.run.auth.settings.github=設定 > バージョン管理 > GitHub -workflow.run.auth.settings.gitea=設定 > ツール > GitHub Workflow +workflow.run.auth.settings.gitea=設定 > バージョン管理 > Gitea workflow.run.auth.add={0} でトークンまたはアカウントを追加。 workflow.log.command=実行: workflow.log.warning=警告: @@ -384,6 +384,7 @@ completion.steps.outcome=エラー時続行が適用される前に完了した completion.jobs.outputs=ジョブに対して定義された出力のセット。 completion.jobs.result=仕事の結果。 settings.displayName=GitHub ワークフロー +settings.gitea.displayName=Gitea settings.language.label=言語: settings.language.system=IDE/システムデフォルト settings.gitea.title=Gitea アカウント diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ko.properties b/src/main/resources/messages/GitHubWorkflowBundle_ko.properties index 586b67a..a7e8cc7 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ko.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ko.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=워크플로 실행 중지 workflow.run.gutter.stop.text=워크플로 실행 중지 workflow.run.gutter.stop.description=이 실행 취소 workflow.run.auth.settings.github=설정 > 버전 관리 > GitHub -workflow.run.auth.settings.gitea=설정 > 도구 > GitHub Workflow +workflow.run.auth.settings.gitea=설정 > 버전 관리 > Gitea workflow.run.auth.add={0}에서 토큰 또는 계정을 추가하세요. workflow.log.command=실행: workflow.log.warning=경고: @@ -384,6 +384,7 @@ completion.steps.outcome=오류 시 계속이 적용되기 전 완료된 단계 completion.jobs.outputs=작업에 대해 정의된 출력 세트입니다. completion.jobs.result=작업의 결과입니다. settings.displayName=GitHub 작업 흐름 +settings.gitea.displayName=Gitea settings.language.label=언어: settings.language.system=IDE/시스템 기본값 settings.gitea.title=Gitea 계정 diff --git a/src/main/resources/messages/GitHubWorkflowBundle_nl.properties b/src/main/resources/messages/GitHubWorkflowBundle_nl.properties index 553748c..9e299b4 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_nl.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_nl.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Stop de werkstroomuitvoering workflow.run.gutter.stop.text=Stop de werkstroomuitvoering workflow.run.gutter.stop.description=Annuleer deze uitvoering workflow.run.auth.settings.github=Instellingen > Versiebeheer > GitHub -workflow.run.auth.settings.gitea=Instellingen > Tools > GitHub Workflow +workflow.run.auth.settings.gitea=Instellingen > Versiebeheer > Gitea workflow.run.auth.add=Voeg een token of account toe in {0}. workflow.log.command=rennen: workflow.log.warning=waarschuwing: @@ -384,6 +384,7 @@ completion.steps.outcome=Het resultaat van een voltooide stap vóór doorgaan bi completion.jobs.outputs=De set uitvoer die voor de taak is gedefinieerd. completion.jobs.result=Het resultaat van de klus. settings.displayName=GitHub-workflow +settings.gitea.displayName=Gitea settings.language.label=Taal: settings.language.system=IDE/systeemstandaard settings.gitea.title=Gitea-accounts diff --git a/src/main/resources/messages/GitHubWorkflowBundle_pl.properties b/src/main/resources/messages/GitHubWorkflowBundle_pl.properties index f95f955..a679fa8 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_pl.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_pl.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Zatrzymaj przebieg przepływu pracy workflow.run.gutter.stop.text=Zatrzymaj przebieg przepływu pracy workflow.run.gutter.stop.description=Anuluj ten bieg workflow.run.auth.settings.github=Ustawienia > Kontrola wersji > GitHub -workflow.run.auth.settings.gitea=Ustawienia > Narzędzia > GitHub Workflow +workflow.run.auth.settings.gitea=Ustawienia > Kontrola wersji > Gitea workflow.run.auth.add=Dodaj token albo konto w {0}. workflow.log.command=biegnij: workflow.log.warning=ostrzeżenie: @@ -384,6 +384,7 @@ completion.steps.outcome=Wynik zakończonego kroku przed zastosowaniem błędu k completion.jobs.outputs=Zestaw wyników zdefiniowany dla zadania. completion.jobs.result=Wynik pracy. settings.displayName=Przebieg pracy GitHub +settings.gitea.displayName=Gitea settings.language.label=Język: settings.language.system=Wartość domyślna IDE/systemowa settings.gitea.title=Konta Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties b/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties index 829e578..db3b982 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Interromper a execução do fluxo de trabalho workflow.run.gutter.stop.text=Interromper a execução do fluxo de trabalho workflow.run.gutter.stop.description=Cancelar esta execução workflow.run.auth.settings.github=Configurações > Controle de versão > GitHub -workflow.run.auth.settings.gitea=Configurações > Ferramentas > GitHub Workflow +workflow.run.auth.settings.gitea=Configurações > Controle de versão > Gitea workflow.run.auth.add=Adicione token ou conta em {0}. workflow.log.command=execute: workflow.log.warning=aviso: @@ -384,6 +384,7 @@ completion.steps.outcome=O resultado de uma etapa concluída antes da aplicaçã completion.jobs.outputs=O conjunto de saídas definidas para o trabalho. completion.jobs.result=O resultado do trabalho. settings.displayName=Fluxo de trabalho GitHub +settings.gitea.displayName=Gitea settings.language.label=Idioma: settings.language.system=IDE/padrão do sistema settings.gitea.title=Contas Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ru.properties b/src/main/resources/messages/GitHubWorkflowBundle_ru.properties index 023410c..0ee3933 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ru.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ru.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Остановить выполнение рабоче workflow.run.gutter.stop.text=Остановить выполнение рабочего процесса workflow.run.gutter.stop.description=Отменить этот запуск workflow.run.auth.settings.github=Настройки > Контроль версий > GitHub -workflow.run.auth.settings.gitea=Настройки > Инструменты > GitHub Workflow +workflow.run.auth.settings.gitea=Настройки > Контроль версий > Gitea workflow.run.auth.add=Добавьте токен или аккаунт в {0}. workflow.log.command=запустить: workflow.log.warning=предупреждение: @@ -384,6 +384,7 @@ completion.steps.outcome=Результат завершенного шага д completion.jobs.outputs=Набор выходных данных, определенных для задания. completion.jobs.result=Результат работы. settings.displayName=GitHub Рабочий процесс +settings.gitea.displayName=Gitea settings.language.label=Язык: settings.language.system=IDE/системное значение по умолчанию settings.gitea.title=Аккаунты Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_sv.properties b/src/main/resources/messages/GitHubWorkflowBundle_sv.properties index 1d165b0..e3c8667 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_sv.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_sv.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Stoppa körningen av arbetsflödet workflow.run.gutter.stop.text=Stoppa körningen av arbetsflödet workflow.run.gutter.stop.description=Avbryt denna körning workflow.run.auth.settings.github=Inställningar > Versionskontroll > GitHub -workflow.run.auth.settings.gitea=Inställningar > Verktyg > GitHub Workflow +workflow.run.auth.settings.gitea=Inställningar > Versionskontroll > Gitea workflow.run.auth.add=Lägg till token eller konto i {0}. workflow.log.command=kör: workflow.log.warning=varning: @@ -384,6 +384,7 @@ completion.steps.outcome=Resultatet av ett avslutat steg innan fortsätt-på-fel completion.jobs.outputs=Uppsättningen utgångar som definierats för jobbet. completion.jobs.result=Resultatet av jobbet. settings.displayName=GitHub arbetsflöde +settings.gitea.displayName=Gitea settings.language.label=Språk: settings.language.system=IDE/systemstandard settings.gitea.title=Gitea-konton diff --git a/src/main/resources/messages/GitHubWorkflowBundle_th.properties b/src/main/resources/messages/GitHubWorkflowBundle_th.properties index 46a2b73..d135f63 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_th.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_th.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=หยุดการรันเวิร์กโฟ workflow.run.gutter.stop.text=หยุดการรันเวิร์กโฟลว์ workflow.run.gutter.stop.description=ยกเลิกการวิ่งครั้งนี้ workflow.run.auth.settings.github=ตั้งค่า > ควบคุมเวอร์ชัน > GitHub -workflow.run.auth.settings.gitea=ตั้งค่า > เครื่องมือ > GitHub Workflow +workflow.run.auth.settings.gitea=ตั้งค่า > ควบคุมเวอร์ชัน > Gitea workflow.run.auth.add=เพิ่มโทเคนหรือบัญชีใน {0}. workflow.log.command=วิ่ง: workflow.log.warning=คำเตือน: @@ -384,6 +384,7 @@ completion.steps.outcome=ผลลัพธ์ของขั้นตอนท completion.jobs.outputs=ชุดเอาต์พุตที่กำหนดไว้สำหรับงาน completion.jobs.result=ผลของงาน. settings.displayName=เวิร์กโฟลว์ GitHub +settings.gitea.displayName=Gitea settings.language.label=ภาษา: settings.language.system=IDE/ค่าเริ่มต้นของระบบ settings.gitea.title=บัญชี Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_tr.properties b/src/main/resources/messages/GitHubWorkflowBundle_tr.properties index ae255ff..85c27f3 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_tr.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_tr.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=İş akışı çalışmasını durdur workflow.run.gutter.stop.text=İş akışı çalışmasını durdur workflow.run.gutter.stop.description=Bu çalıştırmayı iptal et workflow.run.auth.settings.github=Ayarlar > Sürüm Kontrolü > GitHub -workflow.run.auth.settings.gitea=Ayarlar > Araçlar > GitHub Workflow +workflow.run.auth.settings.gitea=Ayarlar > Sürüm Kontrolü > Gitea workflow.run.auth.add={0} içinde token veya hesap ekleyin. workflow.log.command=koş: workflow.log.warning=uyarı: @@ -384,6 +384,7 @@ completion.steps.outcome=Hata durumunda devam etmeden önce tamamlanan bir adım completion.jobs.outputs=İş için tanımlanan çıktılar kümesi. completion.jobs.result=İşin sonucu. settings.displayName=GitHub İş Akışı +settings.gitea.displayName=Gitea settings.language.label=Dil: settings.language.system=IDE/sistem varsayılanı settings.gitea.title=Gitea hesapları diff --git a/src/main/resources/messages/GitHubWorkflowBundle_uk.properties b/src/main/resources/messages/GitHubWorkflowBundle_uk.properties index eac1fd6..4181e40 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_uk.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_uk.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Зупинити запуск робочого проц workflow.run.gutter.stop.text=Зупинити запуск робочого процесу workflow.run.gutter.stop.description=Скасувати цей запуск workflow.run.auth.settings.github=Налаштування > Контроль версій > GitHub -workflow.run.auth.settings.gitea=Налаштування > Інструменти > GitHub Workflow +workflow.run.auth.settings.gitea=Налаштування > Контроль версій > Gitea workflow.run.auth.add=Додайте токен або обліковку в {0}. workflow.log.command=запустити: workflow.log.warning=попередження: @@ -384,6 +384,7 @@ completion.steps.outcome=Результат завершеного кроку д completion.jobs.outputs=Набір результатів, визначених для завдання. completion.jobs.result=Результат роботи. settings.displayName=Робочий процес GitHub +settings.gitea.displayName=Gitea settings.language.label=мова: settings.language.system=IDE/система за замовчуванням settings.gitea.title=Обліковки Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_vi.properties b/src/main/resources/messages/GitHubWorkflowBundle_vi.properties index 2fe7b7d..5cb0f2f 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_vi.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_vi.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=Dừng chạy quy trình công việc workflow.run.gutter.stop.text=Dừng chạy quy trình công việc workflow.run.gutter.stop.description=Hủy lần chạy này workflow.run.auth.settings.github=Cài đặt > Quản lý phiên bản > GitHub -workflow.run.auth.settings.gitea=Cài đặt > Công cụ > GitHub Workflow +workflow.run.auth.settings.gitea=Cài đặt > Quản lý phiên bản > Gitea workflow.run.auth.add=Thêm token hoặc tài khoản trong {0}. workflow.log.command=chạy: workflow.log.warning=cảnh báo: @@ -384,6 +384,7 @@ completion.steps.outcome=Kết quả của một bước đã hoàn thành trư completion.jobs.outputs=Tập hợp các đầu ra được xác định cho công việc. completion.jobs.result=Kết quả của công việc. settings.displayName=Quy trình làm việc của GitHub +settings.gitea.displayName=Gitea settings.language.label=Ngôn ngữ: settings.language.system=IDE/mặc định hệ thống settings.gitea.title=Tài khoản Gitea diff --git a/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties b/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties index 0e6febf..63c9b70 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties @@ -30,7 +30,7 @@ workflow.run.gutter.stop=停止工作流程运行 workflow.run.gutter.stop.text=停止工作流程运行 workflow.run.gutter.stop.description=取消本次运行 workflow.run.auth.settings.github=设置 > 版本控制 > GitHub -workflow.run.auth.settings.gitea=设置 > 工具 > GitHub Workflow +workflow.run.auth.settings.gitea=设置 > 版本控制 > Gitea workflow.run.auth.add=在 {0} 添加令牌或账户。 workflow.log.command=运行: workflow.log.warning=警告: @@ -384,6 +384,7 @@ completion.steps.outcome=应用错误继续之前已完成步骤的结果。 completion.jobs.outputs=为作业定义的输出集。 completion.jobs.result=工作的结果。 settings.displayName=GitHub 工作流程 +settings.gitea.displayName=Gitea settings.language.label=语言: settings.language.system=IDE/系统默认 settings.gitea.title=Gitea 账户 diff --git a/src/test/java/com/github/yunabraska/githubworkflow/entry/PluginWiringTest.java b/src/test/java/com/github/yunabraska/githubworkflow/entry/PluginWiringTest.java index df08958..18450ee 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/entry/PluginWiringTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/entry/PluginWiringTest.java @@ -2,6 +2,9 @@ import com.github.yunabraska.githubworkflow.run.WorkflowRunConfiguration; +import com.github.yunabraska.githubworkflow.settings.GitHubWorkflowSettingsConfigurable; +import com.github.yunabraska.githubworkflow.settings.GiteaSettingsConfigurable; + import com.github.yunabraska.githubworkflow.model.GitHubSchemaProvider; import com.github.yunabraska.githubworkflow.state.GitHubActionCache; @@ -18,9 +21,17 @@ import com.intellij.execution.configurations.ConfigurationTypeUtil; import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import javax.swing.AbstractButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.border.TitledBorder; +import java.awt.Component; +import java.awt.Container; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -125,10 +136,58 @@ public void testSettingsConfigurableUsesLocalizedPluginXmlKey() throws IOExcepti assertThat(pluginXml) .contains("key=\"settings.displayName\"") + .contains("key=\"settings.gitea.displayName\"") + .contains("id=\"github.workflow.gitea.settings\"") + .contains(" comboBox) { + for (int index = 0; index < comboBox.getItemCount(); index++) { + if (text.equals(String.valueOf(comboBox.getItemAt(index)))) { + comboBox.setSelectedIndex(index); + return; + } + } + } + } + throw new AssertionError("Combo item not found: " + text); + } + + private static List visibleTexts(final Component root) { + final List result = new ArrayList<>(); + for (final Component component : components(root)) { + if (component instanceof JLabel label && label.getText() != null && !label.getText().isBlank()) { + result.add(label.getText()); + } + if (component instanceof AbstractButton button && button.getText() != null && !button.getText().isBlank()) { + result.add(button.getText()); + } + if (component instanceof JComponent swing && swing.getBorder() instanceof TitledBorder border && border.getTitle() != null) { + result.add(border.getTitle()); + } + } + return result; + } + + private static List components(final Component root) { + final List result = new ArrayList<>(); + collectComponents(root, result); + return result; + } + + private static void collectComponents(final Component component, final List result) { + result.add(component); + if (component instanceof Container container) { + for (final Component child : container.getComponents()) { + collectComponents(child, result); + } + } + } } diff --git a/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java b/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java index 9ea110c..fbbdbed 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java @@ -321,6 +321,7 @@ public void testGiteaStoredTokenIsTriedBeforeEnvironmentTokens() { ); assertThat(settings.hasGiteaToken(server)).isTrue(); + assertThat(settings.customServers().getFirst().tokenStored).isTrue(); assertThat(authorizations) .extracting(RemoteActionProviders.Authorizations.Authorization::source) .containsExactly("Local Gitea", "LOCAL_GITEA_TOKEN", "GITEA_TOKEN", "anonymous"); @@ -388,6 +389,25 @@ public void testGiteaStoredTokenIsNotSerializedInSettingsState() { state.tokenEnvVar )).doesNotContain("stored-token")); assertThat(settings.getState().servers.getFirst().tokenEnvVar).isEqualTo("LOCAL_GITEA_TOKEN"); + assertThat(settings.getState().servers.getFirst().tokenStored).isTrue(); + } + + public void testGiteaStoredTokenMarkerIsClearedWithToken() { + final RemoteActionProviders.Server server = RemoteActionProviders.Server.gitea( + "Local Gitea", + "http://gitea.local", + "http://gitea.local/api/v1", + "LOCAL_GITEA_TOKEN", + true + ); + final RemoteActionProviders.Settings settings = RemoteActionProviders.Settings.getInstance() + .setCustomServers(List.of(server)) + .setGiteaToken(server, "stored-token"); + + settings.clearGiteaToken(server); + + assertThat(settings.hasGiteaToken(server)).isFalse(); + assertThat(settings.customServers().getFirst().tokenStored).isFalse(); } public void testGithubEnvironmentTokensStillUseBearerAuthorizationScheme() { diff --git a/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java b/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java index a555b49..8f21111 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/i18n/WorkflowMessagesTest.java @@ -109,6 +109,7 @@ public void testLocaleBundleValuesAreTranslatedAndKeepPlaceholders() throws IOEx "error.report.ide", "error.report.os", "error.report.stacktrace", + "settings.gitea.displayName", "completion.shell.powershell" ); final Pattern placeholder = Pattern.compile("\\{\\d+\\}"); @@ -186,6 +187,7 @@ public void testEveryConfiguredLocaleResolvesSettingsAndInspectionMessages() { for (final String suffix : LOCALE_SUFFIXES) { final Locale locale = Locale.forLanguageTag(suffix.replace('_', '-')); assertThat(GitHubWorkflowBundle.messageFor(locale, "settings.displayName")).isNotBlank(); + assertThat(GitHubWorkflowBundle.messageFor(locale, "settings.gitea.displayName")).isEqualTo("Gitea"); assertThat(GitHubWorkflowBundle.messageFor(locale, "inspection.action.delete.invalid", "input", "bad")) .contains("bad"); assertThat(GitHubWorkflowBundle.messageFor(locale, "inspection.output.unused", "artifact")) diff --git a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java index 21e770c..40669e5 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java @@ -349,7 +349,7 @@ public void testDispatchAuthenticationFailureMentionsGithubSettings() throws Exc } } - public void testGiteaDispatchAuthenticationFailureMentionsWorkflowSettings() throws Exception { + public void testGiteaDispatchAuthenticationFailureMentionsGiteaSettings() throws Exception { try (FakeWorkflowRunServer server = new FakeWorkflowRunServer(false, 1)) { final HttpClient httpClient = HttpClient.newHttpClient(); final WorkflowRun client = new WorkflowRun( @@ -361,7 +361,7 @@ public void testGiteaDispatchAuthenticationFailureMentionsWorkflowSettings() thr assertThatExceptionOfType(WorkflowRun.WorkflowRunHttpException.class) .isThrownBy(() -> client.dispatch(request)) .withMessageContaining("GitHub workflow dispatch failed with HTTP 401") - .withMessageContaining("Settings > Tools > GitHub Workflow") + .withMessageContaining("Settings > Version Control > Gitea") .withMessageNotContaining("Settings > Version Control > GitHub"); } } diff --git a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowMetadataTest.java b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowMetadataTest.java index eb24cb8..61f9f3a 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowMetadataTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowMetadataTest.java @@ -5,8 +5,9 @@ import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; -import java.nio.file.Path; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Objects; @@ -146,6 +147,17 @@ public void runnerDebugDescriptionMatchesDocumentedMeaning() { .doesNotContain("preinstalled tools"); } + @Test + public void shellDescriptionsAreLoadedLazily() throws Exception { + final String source = Files.readString(Path.of( + "src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java" + )); + + assertThat(source).doesNotContain("static final Map SHELLS = initShells()"); + assertThat(WorkflowContextCatalog.shells()) + .containsKeys("bash", "sh", "pwsh", "powershell", "cmd", "python"); + } + private static List resourceKeys(final String path) throws Exception { try (InputStream stream = Objects.requireNonNull(WorkflowMetadataTest.class.getResourceAsStream(path)); BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { From e8d682ac46f273e4d09eeb2674585db007462170 Mon Sep 17 00:00:00 2001 From: Yuna Morgenstern Date: Mon, 15 Jun 2026 17:44:17 +0200 Subject: [PATCH 7/8] Harden Gitea settings cleanup --- .../git/RemoteActionProviders.java | 3 +- .../githubworkflow/run/WorkflowRun.java | 2 +- .../run/WorkflowRunProcessHandler.java | 2 +- .../settings/GiteaSettingsConfigurable.java | 23 +++++++++-- .../messages/GitHubWorkflowBundle.properties | 1 + .../GitHubWorkflowBundle_ar.properties | 1 + .../GitHubWorkflowBundle_cs.properties | 1 + .../GitHubWorkflowBundle_de.properties | 1 + .../GitHubWorkflowBundle_es.properties | 1 + .../GitHubWorkflowBundle_fr.properties | 1 + .../GitHubWorkflowBundle_hi.properties | 1 + .../GitHubWorkflowBundle_id.properties | 1 + .../GitHubWorkflowBundle_it.properties | 1 + .../GitHubWorkflowBundle_ja.properties | 1 + .../GitHubWorkflowBundle_ko.properties | 1 + .../GitHubWorkflowBundle_nl.properties | 1 + .../GitHubWorkflowBundle_pl.properties | 1 + .../GitHubWorkflowBundle_pt_BR.properties | 1 + .../GitHubWorkflowBundle_ru.properties | 1 + .../GitHubWorkflowBundle_sv.properties | 1 + .../GitHubWorkflowBundle_th.properties | 1 + .../GitHubWorkflowBundle_tr.properties | 1 + .../GitHubWorkflowBundle_uk.properties | 1 + .../GitHubWorkflowBundle_vi.properties | 1 + .../GitHubWorkflowBundle_zh_CN.properties | 1 + .../entry/PluginWiringTest.java | 40 +++++++++++++++++++ .../git/RemoteActionProvidersTest.java | 16 ++++++++ .../githubworkflow/run/WorkflowRunTest.java | 28 ++++++++----- 28 files changed, 119 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java b/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java index 9f7feae..e18d0f8 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/git/RemoteActionProviders.java @@ -536,6 +536,7 @@ public List enabledServers() { final Map result = new LinkedHashMap<>(); customServers().stream() .map(Server::normalized) + .filter(Server::isEnabled) .filter(Server::isValid) .forEach(server -> result.put(server.key(), server)); jetBrainsGithubServers().forEach(server -> result.putIfAbsent(server.key(), server)); @@ -741,7 +742,7 @@ public boolean isEnabled() { } public boolean isValid() { - return isEnabled() && hasText(webUrl) && hasText(apiUrl); + return hasText(webUrl) && hasText(apiUrl); } /** diff --git a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java index 4428c94..b912e63 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRun.java @@ -452,7 +452,7 @@ private static WorkflowRunHttpException failure( statusCode, body, accountActionRecommended, - server.isGitea() ? "github.workflow.settings" : "GitHub" + server.isGitea() ? "github.workflow.gitea.settings" : "GitHub" ); } diff --git a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java index 0ce3860..b377af2 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/run/WorkflowRunProcessHandler.java @@ -538,7 +538,7 @@ private void notifyAuthenticationHelp(final String settingsId) { .createNotification( GitHubWorkflowBundle.message( "workflow.run.notification.auth", - "github.workflow.settings".equals(settingsId) + "github.workflow.gitea.settings".equals(settingsId) ? GitHubWorkflowBundle.message("workflow.run.auth.settings.gitea") : GitHubWorkflowBundle.message("workflow.run.auth.settings.github") ), diff --git a/src/main/java/com/github/yunabraska/githubworkflow/settings/GiteaSettingsConfigurable.java b/src/main/java/com/github/yunabraska/githubworkflow/settings/GiteaSettingsConfigurable.java index ac88864..c610e22 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/settings/GiteaSettingsConfigurable.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/settings/GiteaSettingsConfigurable.java @@ -106,9 +106,10 @@ public boolean isCellEditable(final int row, final int column) { */ @Override public boolean isModified() { - return !Objects.equals(serversFromTable(), settings.customServers().stream() - .filter(RemoteActionProviders.Server::isGitea) - .toList()) + return invalidRowCount() > 0 + || !Objects.equals(serversFromTable(), settings.customServers().stream() + .filter(RemoteActionProviders.Server::isGitea) + .toList()) || tokenModified(); } @@ -117,6 +118,10 @@ public boolean isModified() { */ @Override public void apply() { + if (invalidRowCount() > 0) { + Messages.showErrorDialog(panel, GitHubWorkflowBundle.message("settings.gitea.invalidRows"), getDisplayName()); + return; + } saveRows(); refreshTexts(); GitHubActionCache.triggerSyntaxHighlightingForActiveFiles(); @@ -278,7 +283,17 @@ private void saveRows() { } private boolean tokenModified() { - return !pendingTokens.isEmpty(); + return !pendingTokens.isEmpty() || !clearedTokens.isEmpty(); + } + + private int invalidRowCount() { + int result = 0; + for (int row = 0; row < model.getRowCount(); row++) { + if (!serverFromRow(row).isValid()) { + result++; + } + } + return result; } private List serversFromTable() { diff --git a/src/main/resources/messages/GitHubWorkflowBundle.properties b/src/main/resources/messages/GitHubWorkflowBundle.properties index 23207f5..db9bbe0 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Remove settings.gitea.setToken=Set token settings.gitea.clearToken=Clear token settings.gitea.noneSelected=Select a Gitea row first. +settings.gitea.invalidRows=Fix invalid Gitea rows first. Name, web URL, and API URL need text. settings.gitea.token.prompt=Paste Gitea token settings.cache.title=Action cache settings.cache.column.key=Cache key diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ar.properties b/src/main/resources/messages/GitHubWorkflowBundle_ar.properties index cdfa070..f33c295 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ar.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ar.properties @@ -399,6 +399,7 @@ settings.gitea.remove=إزالة settings.gitea.setToken=تعيين الرمز settings.gitea.clearToken=مسح الرمز settings.gitea.noneSelected=اختر صف Gitea أولا. +settings.gitea.invalidRows=أصلح صفوف Gitea غير الصالحة أولا. الاسم ورابط الويب ورابط API تحتاج نصا. settings.gitea.token.prompt=الصق رمز Gitea settings.cache.title=ذاكرة التخزين المؤقت للعمل settings.cache.column.key=مفتاح ذاكرة التخزين المؤقت diff --git a/src/main/resources/messages/GitHubWorkflowBundle_cs.properties b/src/main/resources/messages/GitHubWorkflowBundle_cs.properties index 3185f1b..b8277c7 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_cs.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_cs.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Odebrat settings.gitea.setToken=Nastavit token settings.gitea.clearToken=Smazat token settings.gitea.noneSelected=Nejdřív vyber řádek Gitea. +settings.gitea.invalidRows=Nejdřív oprav neplatné řádky Gitea. Název, webová URL a API URL potřebují text. settings.gitea.token.prompt=Vlož token Gitea settings.cache.title=Mezipaměť akcí settings.cache.column.key=Klíč mezipaměti diff --git a/src/main/resources/messages/GitHubWorkflowBundle_de.properties b/src/main/resources/messages/GitHubWorkflowBundle_de.properties index d0aad10..bfb2cdb 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_de.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_de.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Entfernen settings.gitea.setToken=Token setzen settings.gitea.clearToken=Token löschen settings.gitea.noneSelected=Erst eine Gitea-Zeile wählen. +settings.gitea.invalidRows=Ungültige Gitea-Zeilen erst reparieren. Name, Web-URL und API-URL brauchen Text. settings.gitea.token.prompt=Gitea-Token einfügen settings.cache.title=Aktionscache settings.cache.column.key=Cache-Schlüssel diff --git a/src/main/resources/messages/GitHubWorkflowBundle_es.properties b/src/main/resources/messages/GitHubWorkflowBundle_es.properties index 0ec24cb..fafe8c5 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_es.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_es.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Quitar settings.gitea.setToken=Guardar token settings.gitea.clearToken=Borrar token settings.gitea.noneSelected=Selecciona primero una fila Gitea. +settings.gitea.invalidRows=Arregla primero las filas Gitea inválidas. Nombre, URL web y URL API necesitan texto. settings.gitea.token.prompt=Pega el token de Gitea settings.cache.title=Caché de acciones settings.cache.column.key=clave de caché diff --git a/src/main/resources/messages/GitHubWorkflowBundle_fr.properties b/src/main/resources/messages/GitHubWorkflowBundle_fr.properties index 8f4596d..2818ee4 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_fr.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_fr.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Retirer settings.gitea.setToken=Définir jeton settings.gitea.clearToken=Effacer jeton settings.gitea.noneSelected=Sélectionnez d’abord une ligne Gitea. +settings.gitea.invalidRows=Corrigez d’abord les lignes Gitea invalides. Nom, URL web et URL API doivent contenir du texte. settings.gitea.token.prompt=Collez le jeton Gitea settings.cache.title=Cache d''actions settings.cache.column.key=Clé de cache diff --git a/src/main/resources/messages/GitHubWorkflowBundle_hi.properties b/src/main/resources/messages/GitHubWorkflowBundle_hi.properties index 742e868..6e7d1ed 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_hi.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_hi.properties @@ -399,6 +399,7 @@ settings.gitea.remove=हटाएं settings.gitea.setToken=टोकन सेट करें settings.gitea.clearToken=टोकन साफ करें settings.gitea.noneSelected=पहले Gitea पंक्ति चुनें. +settings.gitea.invalidRows=पहले अमान्य Gitea पंक्तियां ठीक करें. नाम, वेब URL और API URL चाहिए. settings.gitea.token.prompt=Gitea टोकन चिपकाएं settings.cache.title=एक्शन कैश settings.cache.column.key=कैश कुंजी diff --git a/src/main/resources/messages/GitHubWorkflowBundle_id.properties b/src/main/resources/messages/GitHubWorkflowBundle_id.properties index 8cb84bd..1e290ea 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_id.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_id.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Hapus settings.gitea.setToken=Atur token settings.gitea.clearToken=Bersihkan token settings.gitea.noneSelected=Pilih baris Gitea dulu. +settings.gitea.invalidRows=Perbaiki baris Gitea yang tidak valid dulu. Nama, URL web, dan URL API wajib diisi. settings.gitea.token.prompt=Tempel token Gitea settings.cache.title=Tembolok tindakan settings.cache.column.key=Kunci cache diff --git a/src/main/resources/messages/GitHubWorkflowBundle_it.properties b/src/main/resources/messages/GitHubWorkflowBundle_it.properties index f096812..a629734 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_it.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_it.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Rimuovi settings.gitea.setToken=Imposta token settings.gitea.clearToken=Cancella token settings.gitea.noneSelected=Seleziona prima una riga Gitea. +settings.gitea.invalidRows=Correggi prima le righe Gitea non valide. Nome, URL web e URL API vogliono testo. settings.gitea.token.prompt=Incolla token Gitea settings.cache.title=Cache delle azioni settings.cache.column.key=Chiave della cache diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ja.properties b/src/main/resources/messages/GitHubWorkflowBundle_ja.properties index b9c2337..e754dfd 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ja.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ja.properties @@ -399,6 +399,7 @@ settings.gitea.remove=削除 settings.gitea.setToken=トークン設定 settings.gitea.clearToken=トークン消去 settings.gitea.noneSelected=先に Gitea 行を選択。 +settings.gitea.invalidRows=無効な Gitea 行を先に直してください。名前、Web URL、API URL が必要です。 settings.gitea.token.prompt=Gitea トークンを貼り付け settings.cache.title=アクションキャッシュ settings.cache.column.key=キャッシュキー diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ko.properties b/src/main/resources/messages/GitHubWorkflowBundle_ko.properties index a7e8cc7..e8b8d23 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ko.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ko.properties @@ -399,6 +399,7 @@ settings.gitea.remove=제거 settings.gitea.setToken=토큰 설정 settings.gitea.clearToken=토큰 지우기 settings.gitea.noneSelected=먼저 Gitea 행을 선택하세요. +settings.gitea.invalidRows=잘못된 Gitea 행을 먼저 고치세요. 이름, 웹 URL, API URL이 필요합니다. settings.gitea.token.prompt=Gitea 토큰 붙여넣기 settings.cache.title=액션 캐시 settings.cache.column.key=캐시 키 diff --git a/src/main/resources/messages/GitHubWorkflowBundle_nl.properties b/src/main/resources/messages/GitHubWorkflowBundle_nl.properties index 9e299b4..a66adbd 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_nl.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_nl.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Verwijderen settings.gitea.setToken=Token instellen settings.gitea.clearToken=Token wissen settings.gitea.noneSelected=Selecteer eerst een Gitea-rij. +settings.gitea.invalidRows=Repareer eerst ongeldige Gitea-rijen. Naam, web-URL en API-URL hebben tekst nodig. settings.gitea.token.prompt=Plak Gitea-token settings.cache.title=Actiecache settings.cache.column.key=Cachesleutel diff --git a/src/main/resources/messages/GitHubWorkflowBundle_pl.properties b/src/main/resources/messages/GitHubWorkflowBundle_pl.properties index a679fa8..bc9ccbc 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_pl.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_pl.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Usuń settings.gitea.setToken=Ustaw token settings.gitea.clearToken=Wyczyść token settings.gitea.noneSelected=Najpierw wybierz wiersz Gitea. +settings.gitea.invalidRows=Najpierw napraw nieprawidłowe wiersze Gitea. Nazwa, URL WWW i URL API wymagają tekstu. settings.gitea.token.prompt=Wklej token Gitea settings.cache.title=Pamięć akcji settings.cache.column.key=Klucz pamięci podręcznej diff --git a/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties b/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties index db3b982..543b152 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_pt_BR.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Remover settings.gitea.setToken=Definir token settings.gitea.clearToken=Limpar token settings.gitea.noneSelected=Selecione uma linha Gitea primeiro. +settings.gitea.invalidRows=Corrija primeiro as linhas Gitea inválidas. Nome, URL web e URL API precisam de texto. settings.gitea.token.prompt=Cole o token Gitea settings.cache.title=Cache de ação settings.cache.column.key=Chave de cache diff --git a/src/main/resources/messages/GitHubWorkflowBundle_ru.properties b/src/main/resources/messages/GitHubWorkflowBundle_ru.properties index 0ee3933..c744dd2 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_ru.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_ru.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Удалить settings.gitea.setToken=Задать токен settings.gitea.clearToken=Очистить токен settings.gitea.noneSelected=Сначала выберите строку Gitea. +settings.gitea.invalidRows=Сначала исправьте неверные строки Gitea. Имя, web URL и API URL должны быть заполнены. settings.gitea.token.prompt=Вставьте токен Gitea settings.cache.title=Кэш действий settings.cache.column.key=Ключ кэша diff --git a/src/main/resources/messages/GitHubWorkflowBundle_sv.properties b/src/main/resources/messages/GitHubWorkflowBundle_sv.properties index e3c8667..d861b2e 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_sv.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_sv.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Ta bort settings.gitea.setToken=Sätt token settings.gitea.clearToken=Rensa token settings.gitea.noneSelected=Välj en Gitea-rad först. +settings.gitea.invalidRows=Fixa ogiltiga Gitea-rader först. Namn, webbadress och API-adress behöver text. settings.gitea.token.prompt=Klistra in Gitea-token settings.cache.title=Åtgärdscache settings.cache.column.key=Cache-nyckel diff --git a/src/main/resources/messages/GitHubWorkflowBundle_th.properties b/src/main/resources/messages/GitHubWorkflowBundle_th.properties index d135f63..467f2f5 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_th.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_th.properties @@ -399,6 +399,7 @@ settings.gitea.remove=ลบ settings.gitea.setToken=ตั้งโทเคน settings.gitea.clearToken=ล้างโทเคน settings.gitea.noneSelected=เลือกแถว Gitea ก่อน. +settings.gitea.invalidRows=แก้แถว Gitea ที่ไม่ถูกต้องก่อน ชื่อ, URL เว็บ และ URL API ต้องมีข้อความ settings.gitea.token.prompt=วางโทเคน Gitea settings.cache.title=แคชการดำเนินการ settings.cache.column.key=รหัสแคช diff --git a/src/main/resources/messages/GitHubWorkflowBundle_tr.properties b/src/main/resources/messages/GitHubWorkflowBundle_tr.properties index 85c27f3..16c9813 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_tr.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_tr.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Kaldır settings.gitea.setToken=Token ayarla settings.gitea.clearToken=Token temizle settings.gitea.noneSelected=Önce bir Gitea satırı seçin. +settings.gitea.invalidRows=Önce geçersiz Gitea satırlarını düzelt. Ad, web URL ve API URL metin ister. settings.gitea.token.prompt=Gitea tokenini yapıştır settings.cache.title=Eylem önbelleği settings.cache.column.key=Önbellek anahtarı diff --git a/src/main/resources/messages/GitHubWorkflowBundle_uk.properties b/src/main/resources/messages/GitHubWorkflowBundle_uk.properties index 4181e40..2cbedb9 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_uk.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_uk.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Вилучити settings.gitea.setToken=Задати токен settings.gitea.clearToken=Очистити токен settings.gitea.noneSelected=Спершу виберіть рядок Gitea. +settings.gitea.invalidRows=Спершу виправте хибні рядки Gitea. Назва, web URL і API URL мають бути заповнені. settings.gitea.token.prompt=Вставте токен Gitea settings.cache.title=Кеш дій settings.cache.column.key=Ключ кешу diff --git a/src/main/resources/messages/GitHubWorkflowBundle_vi.properties b/src/main/resources/messages/GitHubWorkflowBundle_vi.properties index 5cb0f2f..09c35e8 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_vi.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_vi.properties @@ -399,6 +399,7 @@ settings.gitea.remove=Xóa settings.gitea.setToken=Đặt token settings.gitea.clearToken=Xóa token settings.gitea.noneSelected=Chọn một dòng Gitea trước. +settings.gitea.invalidRows=Sửa các dòng Gitea sai trước. Tên, URL web và URL API cần có chữ. settings.gitea.token.prompt=Dán token Gitea settings.cache.title=Bộ đệm hành động settings.cache.column.key=Khóa bộ đệm diff --git a/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties b/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties index 63c9b70..27cc1c0 100644 --- a/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties +++ b/src/main/resources/messages/GitHubWorkflowBundle_zh_CN.properties @@ -399,6 +399,7 @@ settings.gitea.remove=移除 settings.gitea.setToken=设置令牌 settings.gitea.clearToken=清除令牌 settings.gitea.noneSelected=先选择一个 Gitea 行。 +settings.gitea.invalidRows=先修好无效的 Gitea 行。名称、网页 URL 和 API URL 都要有内容。 settings.gitea.token.prompt=粘贴 Gitea 令牌 settings.cache.title=动作缓存 settings.cache.column.key=缓存键 diff --git a/src/test/java/com/github/yunabraska/githubworkflow/entry/PluginWiringTest.java b/src/test/java/com/github/yunabraska/githubworkflow/entry/PluginWiringTest.java index 18450ee..d65146c 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/entry/PluginWiringTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/entry/PluginWiringTest.java @@ -2,6 +2,8 @@ import com.github.yunabraska.githubworkflow.run.WorkflowRunConfiguration; +import com.github.yunabraska.githubworkflow.git.RemoteActionProviders; + import com.github.yunabraska.githubworkflow.settings.GitHubWorkflowSettingsConfigurable; import com.github.yunabraska.githubworkflow.settings.GiteaSettingsConfigurable; @@ -25,6 +27,7 @@ import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; +import javax.swing.JTable; import javax.swing.border.TitledBorder; import java.awt.Component; import java.awt.Container; @@ -188,6 +191,25 @@ public void testGiteaSettingsConfigurableIsLocalizedUnderVersionControl() { } } + public void testGiteaSettingsInvalidRowsKeepApplyAvailable() { + final RemoteActionProviders.Settings remoteSettings = RemoteActionProviders.Settings.getInstance(); + final List previousServers = remoteSettings.customServers(); + final GiteaSettingsConfigurable configurable = new GiteaSettingsConfigurable(); + try { + remoteSettings.setCustomServers(List.of()); + final JComponent component = configurable.createComponent(); + findButton(component, GitHubWorkflowBundle.message("settings.gitea.add")).doClick(); + final JTable table = findTable(component); + + table.setValueAt("", 0, 3); + + assertThat(configurable.isModified()).isTrue(); + } finally { + configurable.disposeUIResources(); + remoteSettings.setCustomServers(previousServers); + } + } + public void testPackagedSchemasArePresentAndNonEmpty() throws IOException { final Path directory = Path.of(System.getProperty("user.dir"), "src", "main", "resources", "schemas"); @@ -215,6 +237,24 @@ private static void selectComboItem(final Component root, final String text) { throw new AssertionError("Combo item not found: " + text); } + private static AbstractButton findButton(final Component root, final String text) { + for (final Component component : components(root)) { + if (component instanceof AbstractButton button && text.equals(button.getText())) { + return button; + } + } + throw new AssertionError("Button not found: " + text); + } + + private static JTable findTable(final Component root) { + for (final Component component : components(root)) { + if (component instanceof JTable table) { + return table; + } + } + throw new AssertionError("Table not found"); + } + private static List visibleTexts(final Component root) { final List result = new ArrayList<>(); for (final Component component : components(root)) { diff --git a/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java b/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java index fbbdbed..353fb19 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/git/RemoteActionProvidersTest.java @@ -410,6 +410,22 @@ public void testGiteaStoredTokenMarkerIsClearedWithToken() { assertThat(settings.customServers().getFirst().tokenStored).isFalse(); } + public void testDisabledGiteaServerIsStoredButNotUsed() { + final RemoteActionProviders.Server server = RemoteActionProviders.Server.gitea( + "Quiet Gitea", + "http://gitea.local", + "http://gitea.local/api/v1", + "LOCAL_GITEA_TOKEN", + false + ); + final RemoteActionProviders.Settings settings = RemoteActionProviders.Settings.getInstance() + .setCustomServers(List.of(server)); + + assertThat(settings.customServers()).containsExactly(server.normalized()); + assertThat(settings.enabledServers()) + .noneMatch(candidate -> "http://gitea.local/api/v1".equals(candidate.apiUrl)); + } + public void testGithubEnvironmentTokensStillUseBearerAuthorizationScheme() { final RemoteActionProviders.Server server = new RemoteActionProviders.Server( "GitHub", diff --git a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java index 40669e5..94ebeeb 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowRunTest.java @@ -342,10 +342,15 @@ public void testDispatchAuthenticationFailureMentionsGithubSettings() throws Exc ); final WorkflowRun.Request request = new WorkflowRun.Request(server.apiUrl(), "acme", "tool", "build.yml", "main", Map.of(), ""); - assertThatExceptionOfType(WorkflowRun.WorkflowRunHttpException.class) - .isThrownBy(() -> client.dispatch(request)) - .withMessageContaining("GitHub workflow dispatch failed with HTTP 401") - .withMessageContaining("Settings > Version Control > GitHub"); + try { + client.dispatch(request); + fail("Expected workflow dispatch to require authentication"); + } catch (final WorkflowRun.WorkflowRunHttpException exception) { + assertThat(exception.getMessage()) + .contains("GitHub workflow dispatch failed with HTTP 401") + .contains("Settings > Version Control > GitHub"); + assertThat(exception.settingsId()).isEqualTo("GitHub"); + } } } @@ -358,11 +363,16 @@ public void testGiteaDispatchAuthenticationFailureMentionsGiteaSettings() throws ); final WorkflowRun.Request request = new WorkflowRun.Request(server.apiUrl().replace("/api", "/api/v1"), "acme", "tool", ".gitea/workflows/build.yml", "main", Map.of(), ""); - assertThatExceptionOfType(WorkflowRun.WorkflowRunHttpException.class) - .isThrownBy(() -> client.dispatch(request)) - .withMessageContaining("GitHub workflow dispatch failed with HTTP 401") - .withMessageContaining("Settings > Version Control > Gitea") - .withMessageNotContaining("Settings > Version Control > GitHub"); + try { + client.dispatch(request); + fail("Expected Gitea workflow dispatch to require authentication"); + } catch (final WorkflowRun.WorkflowRunHttpException exception) { + assertThat(exception.getMessage()) + .contains("GitHub workflow dispatch failed with HTTP 401") + .contains("Settings > Version Control > Gitea") + .doesNotContain("Settings > Version Control > GitHub"); + assertThat(exception.settingsId()).isEqualTo("github.workflow.gitea.settings"); + } } } From f89d8157211fbb06acfb165e60bb4d86abf9b67c Mon Sep 17 00:00:00 2001 From: Yuna Morgenstern Date: Sat, 20 Jun 2026 10:24:59 +0200 Subject: [PATCH 8/8] Harden local Docker lookup for Gitea tests --- README.md | 4 +- .../git/GiteaDockerIntegrationTest.java | 123 +++++++++++++++--- 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ceb7a27..c7cf354 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,9 @@ GITEA_DOCKER_TEST=false ./gradlew test That starts the official rootless Gitea Docker image, seeds a tiny repository, and checks action plus `.gitea/workflows` metadata through the same remote resolver. Set `GITEA_DOCKER_TEST=false` to skip it locally, or override the image with -`GITEA_IMAGE` when testing another Gitea release. +`GITEA_IMAGE` when testing another Gitea release. If `docker` is not on the Gradle process `PATH`, set +`GITEA_DOCKER_BIN`, `DOCKER_BIN`, or `DOCKER_CLI` to the Docker executable; the test also tries `command -v docker`, +`which docker`, `where docker`, and common Docker Desktop/Homebrew locations. ## Release Automation diff --git a/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java b/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java index 2ed1ad0..511467d 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/git/GiteaDockerIntegrationTest.java @@ -13,8 +13,10 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.time.Duration; +import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.Map; @@ -33,6 +35,7 @@ public class GiteaDockerIntegrationTest extends BasePlatformTestCase { private static final String RUNNER_IMAGE = Optional.ofNullable(System.getenv("GITEA_RUNNER_IMAGE")) .filter(value -> !value.isBlank()) .orElse("docker.io/gitea/act_runner:0.6.1"); + private static final List DOCKER_ENV_NAMES = List.of("GITEA_DOCKER_BIN", "DOCKER_BIN", "DOCKER_CLI"); private static final HttpClient CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); @@ -180,7 +183,7 @@ private record RunnerContainer(String id) implements AutoCloseable { @Override public void close() throws Exception { - run("docker", "rm", "-f", id); + run(docker(), "rm", "-f", id); } } @@ -190,8 +193,8 @@ static GiteaContainer start() throws Exception { final String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 12); final String network = "gwplugin-" + suffix; final String name = "gwplugin-gitea-" + suffix; - run("docker", "network", "create", network); - final String id = run("docker", "run", "--rm", "-d", + run(docker(), "network", "create", network); + final String id = run(docker(), "run", "--rm", "-d", "--name", name, "--network", network, "-p", "127.0.0.1::3000", @@ -204,7 +207,7 @@ static GiteaContainer start() throws Exception { "-e", "GITEA__actions__ENABLED=true", IMAGE ).trim(); - final String port = run("docker", "port", id, "3000/tcp").trim().replaceFirst(".*:", ""); + final String port = run(docker(), "port", id, "3000/tcp").trim().replaceFirst(".*:", ""); final GiteaContainer container = new GiteaContainer(id, "http://127.0.0.1:" + port); container.waitUntilReady(); return container; @@ -216,7 +219,7 @@ String apiUrl() { RunnerContainer startRunner() throws Exception { final String runnerName = "gwplugin-runner-" + UUID.randomUUID().toString().replace("-", "").substring(0, 12); - final String runnerToken = run("docker", "exec", id, "gitea", "actions", "generate-runner-token") + final String runnerToken = run(docker(), "exec", id, "gitea", "actions", "generate-runner-token") .lines() .reduce((first, second) -> second) .orElse("") @@ -224,8 +227,8 @@ RunnerContainer startRunner() throws Exception { if (runnerToken.isBlank()) { throw new IllegalStateException("Gitea runner token was not printed"); } - final String network = run("docker", "inspect", "-f", "{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{end}}", id).trim(); - run("docker", "run", "-d", + final String network = run(docker(), "inspect", "-f", "{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{end}}", id).trim(); + run(docker(), "run", "-d", "--name", runnerName, "--network", network, "-e", "GITEA_INSTANCE_URL=http://" + containerName() + ":3000", @@ -240,18 +243,18 @@ RunnerContainer startRunner() throws Exception { } private String containerName() throws Exception { - return run("docker", "inspect", "-f", "{{.Name}}", id).trim().replaceFirst("^/", ""); + return run(docker(), "inspect", "-f", "{{.Name}}", id).trim().replaceFirst("^/", ""); } String createAdminToken() throws Exception { - run("docker", "exec", id, "gitea", "admin", "user", "create", + run(docker(), "exec", id, "gitea", "admin", "user", "create", "--username", "test", "--password", "test-password", "--email", "test@example.com", "--admin", "--must-change-password=false" ); - final String output = run("docker", "exec", id, "gitea", "admin", "user", "generate-access-token", + final String output = run(docker(), "exec", id, "gitea", "admin", "user", "generate-access-token", "--username", "test", "--token-name", "plugin-test", "--scopes", "all" @@ -313,23 +316,23 @@ private void waitUntilReady() throws Exception { private static void waitUntilRunnerReady(final String runnerName) throws Exception { final Pattern ready = Pattern.compile("(?i)(runner registered successfully|declare successfully)"); for (int attempt = 0; attempt < 45; attempt++) { - final String logs = runCombined("docker", "logs", runnerName); + final String logs = runCombined(docker(), "logs", runnerName); if (ready.matcher(logs).find()) { return; } Thread.sleep(1_000); } - throw new IllegalStateException("Gitea runner did not become ready: " + runCombined("docker", "logs", runnerName)); + throw new IllegalStateException("Gitea runner did not become ready: " + runCombined(docker(), "logs", runnerName)); } @Override public void close() throws Exception { - final String network = run("docker", "inspect", "-f", "{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{end}}", id).trim(); + final String network = run(docker(), "inspect", "-f", "{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{end}}", id).trim(); try { - run("docker", "stop", id); + run(docker(), "stop", id); } finally { if (!network.isBlank()) { - run("docker", "network", "rm", network); + run(docker(), "network", "rm", network); } } } @@ -339,6 +342,96 @@ private static String encode(final String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20"); } + private static String docker() throws IOException, InterruptedException { + final List candidates = new ArrayList<>(); + DOCKER_ENV_NAMES.stream() + .map(System::getenv) + .filter(value -> value != null && !value.isBlank()) + .forEach(candidates::add); + probe("sh", "-lc", "command -v docker").ifPresent(candidates::add); + probe("sh", "-lc", "which docker").ifPresent(candidates::add); + probe("cmd", "/c", "where docker").ifPresent(candidates::add); + commonDockerPaths().forEach(candidates::add); + + return candidates.stream() + .map(String::trim) + .filter(value -> !value.isBlank()) + .flatMap(value -> value.lines().map(String::trim)) + .filter(value -> !value.isBlank()) + .distinct() + .filter(GiteaDockerIntegrationTest::canRunDocker) + .findFirst() + .orElseThrow(() -> new IOException(""" + Docker CLI was not found for the Gitea integration test. + Set GITEA_DOCKER_BIN, DOCKER_BIN, or DOCKER_CLI to the docker executable, + or make `docker` visible to command -v / which / where. + """.strip())); + } + + private static List commonDockerPaths() { + final String home = System.getProperty("user.home", ""); + return List.of( + "/opt/homebrew/opt/docker/bin/docker", + "/opt/homebrew/bin/docker", + "/usr/local/bin/docker", + "/Applications/Docker.app/Contents/Resources/bin/docker", + "/Applications/OrbStack.app/Contents/MacOS/docker", + "/Applications/Rancher Desktop.app/Contents/Resources/resources/darwin/bin/docker", + home + "/.docker/bin/docker", + home + "/.rd/bin/docker", + home + "/.orbstack/bin/docker", + home + "/Applications/Docker.app/Contents/Resources/bin/docker", + home + "/Applications/OrbStack.app/Contents/MacOS/docker" + ); + } + + private static Optional probe(final String... command) { + try { + final Process process = new ProcessBuilder(command) + .redirectError(ProcessBuilder.Redirect.DISCARD) + .start(); + final String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim(); + if (process.waitFor(10, java.util.concurrent.TimeUnit.SECONDS) && process.exitValue() == 0 && !output.isBlank()) { + return output.lines().map(String::trim).filter(value -> !value.isBlank()).findFirst(); + } + process.destroyForcibly(); + } catch (final IOException ignored) { + // Candidate discovery is best effort. + } catch (final InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + return Optional.empty(); + } + + private static boolean canRunDocker(final String command) { + if (!isExecutable(command)) { + return false; + } + try { + final Process process = new ProcessBuilder(command, "version") + .redirectOutput(ProcessBuilder.Redirect.DISCARD) + .redirectError(ProcessBuilder.Redirect.DISCARD) + .start(); + if (process.waitFor(10, java.util.concurrent.TimeUnit.SECONDS)) { + return process.exitValue() == 0; + } + process.destroyForcibly(); + } catch (final IOException ignored) { + // Candidate validation is best effort. + } catch (final InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + return false; + } + + private static boolean isExecutable(final String command) { + try { + return Files.isExecutable(Path.of(command)); + } catch (final InvalidPathException ignored) { + return false; + } + } + private static String run(final String... command) throws IOException, InterruptedException { final Path errorLog = Files.createTempFile("gitea-docker-", ".err"); try {