# Development ## Job log line format Gitea stores one log row per line and its web UI decodes the payload, so getting the encoding wrong never fails a test here, it only shows up in the browser. **A row cannot contain a real newline.** `FormatLog` rewrites `\n` to a literal backslash-n and truncates at 64 KiB on a byte boundary. **The payload of a line starting with a recognised prefix is decoded**, with the escape set depending on the prefix: | prefix | decodes | | --- | --- | | `##[error]` `##[warning]` `##[notice]` `##[debug]` `##[group]` `##[endgroup]` `##[add-matcher]` | `%25` `%0D` `%0A` `%3B` `%5D` | | `::error::` `::warning::` `::notice::` `::debug::` (with or without ` key=value` properties), `::group::` `::endgroup::` `::add-matcher::` | `%25` `%0D` `%0A` | | `##[command]` `[command]`, or no recognised prefix | nothing | ### Rules - **Emitting a command line?** Escape the payload with `runner.EscapeCommandData`. One escaper covers both forms: it escapes `%` first, so a literal `%3B` becomes `%253B` that the extra `##[…]` rules cannot match, and a raw `;` or `]` is never decoded. It is also what makes multi-line work, `\n` becomes `%0A` and the UI turns it back into a line break. - **Forwarding a command from step output?** Leave the payload alone, it arrived escaped and is decoded once. Decoding here double-decodes and destroys multi-line. - **No prefix?** Do not escape, and split multi-line values into one row each. - **Interpolating a secret?** Masking runs after escaping, so `AppendSecretMasker` registers the encoded forms too. - Command *properties* also escape `%3A` and `%2C`, which the UI never decodes, so the command handler folds an annotation's location into its decoded message and escapes the result again. ## Logging Log through `act/common/logging`; the linter rejects importing `log/slog` elsewhere. A job's logger comes from `common.Logger(ctx)`; code with no job uses the package functions (`logging.Infof`, `logging.With`). Both take printf-style messages, and `With("key", value)` adds fields. - The process-wide logger masks the secrets of every live task, the job logger masks its own. Going around either leaks them. - The job logger's output can reach the job log workflow authors see, the process-wide logger's stays on the runner host, so keep operator-only detail (host paths, internal addresses) off the job logger. - Libraries that log through slog's global logger are forwarded to the process-wide one, so they follow its level and masking too. Avoid dependencies that log through logrus, nothing forwards it. - Tests read what was logged with `logtest.NewLogger()`, or `logtest.SetDefault(t)` for the package functions. ## Built-in actions Each lives in `internal/pkg/action/` and is registered in `builtinActions` in `act/runner/step_action_builtin.go`. ## End-to-end compatibility tests `make test-e2e` runs the runner against `E2E_GITEA_IMAGE`. It defaults to the nightly image. It requires Docker and is excluded from `make test`. CI runs stable and nightly variants in parallel. Outside a container it runs `go test` in a Linux container on its own network, as CI does, so both take the same paths. `E2E_ARGS` passes extra flags, e.g. `make test-e2e E2E_ARGS='-run TestCompatibility/dispatch'`. The `cli_*` scenarios build the binary and drive `register` and `daemon` as a user would, including `--once`, `--labels` and shutdown with a running job. `failure_paths` covers failed, skipped and tolerated jobs, `job_timeout` a job killed at the runner's deadline. `label_routing` and `capacity` cover scheduling, `reconnect` drops the connection to Gitea through a proxy, and `host_mode` and `reusable_workflow` cover host labels and `workflow_call`. `dispatch`, `failure_paths` and `reusable_workflow` run in every mode: through the daemon and `exec`, each on Docker and Kubernetes. The suite shares one Gitea, regular runner and k3s cluster. Cache and ephemeral scenarios use isolated repository runners. A run path is `@` and a log row is `Z `.