diff --git a/apps/fabro-web/app/components/automation-form.tsx b/apps/fabro-web/app/components/automation-form.tsx
index b7970e744..77bc90903 100644
--- a/apps/fabro-web/app/components/automation-form.tsx
+++ b/apps/fabro-web/app/components/automation-form.tsx
@@ -188,13 +188,16 @@ export function targetFromFormValues(values: AutomationFormValues): GitRunTarget
};
}
+function isWorkflowSourceRefValid(kind: AutomationGitWorkflowSourceKind, ref: string): boolean {
+ const reference = ref.trim();
+ return kind === "commit" ? GIT_SHA_RE.test(reference) : reference !== "";
+}
+
function isWorkflowSourceValid(values: AutomationFormValues): boolean {
if (!values.usesSeparateWorkflowSource) return true;
- const reference = values.workflowSourceRef.trim();
return (
values.workflowSourceRepository.trim() !== "" &&
- reference !== "" &&
- (values.workflowSourceKind !== "commit" || GIT_SHA_RE.test(reference))
+ isWorkflowSourceRefValid(values.workflowSourceKind, values.workflowSourceRef)
);
}
@@ -276,34 +279,38 @@ function describeCron(expression: string): string {
return "Computed when saved";
}
-function workflowSourceRefLabel(kind: AutomationGitWorkflowSourceKind): string {
- switch (kind) {
- case "branch": return "Branch";
- case "tag": return "Tag";
- case "commit": return "Exact commit";
- }
+interface WorkflowSourceKindCopy {
+ label: string;
+ placeholder: string;
+ help: string;
}
-function workflowSourceRefPlaceholder(kind: AutomationGitWorkflowSourceKind): string {
- switch (kind) {
- case "branch": return "main";
- case "tag": return "v1.2.3";
- case "commit": return "0123456789abcdef0123456789abcdef01234567";
- }
-}
+const WORKFLOW_SOURCE_KINDS: Record = {
+ branch: {
+ label: "Branch",
+ placeholder: "main",
+ help: "Bare branch name resolved again whenever the automation fires.",
+ },
+ tag: {
+ label: "Tag",
+ placeholder: "v1.2.3",
+ help: "Bare tag name resolved again whenever the automation fires.",
+ },
+ commit: {
+ label: "Exact commit",
+ placeholder: "0123456789abcdef0123456789abcdef01234567",
+ help: "Exactly 40 hexadecimal characters; the same workflow bytes are used every time.",
+ },
+};
function workflowSourceRefHelp(
kind: AutomationGitWorkflowSourceKind,
valid: boolean,
): ReactNode {
- if (kind === "commit") {
- return valid
- ? "Exactly 40 hexadecimal characters; the same workflow bytes are used every time."
- : Enter exactly 40 hexadecimal characters.;
+ if (kind === "commit" && !valid) {
+ return Enter exactly 40 hexadecimal characters.;
}
- return kind === "branch"
- ? "Bare branch name resolved again whenever the automation fires."
- : "Bare tag name resolved again whenever the automation fires.";
+ return WORKFLOW_SOURCE_KINDS[kind].help;
}
interface AutomationFormFieldsProps {
@@ -325,9 +332,11 @@ export function AutomationFormFields({
}: AutomationFormFieldsProps) {
const slugTouchedRef = useRef(values.id.length > 0);
const shaValid = isOptionalShaValid(values.targetSha);
- const workflowSourceRefValid = values.workflowSourceKind !== "commit"
- ? values.workflowSourceRef.trim() !== ""
- : GIT_SHA_RE.test(values.workflowSourceRef.trim());
+ const workflowSourceRefValid = isWorkflowSourceRefValid(
+ values.workflowSourceKind,
+ values.workflowSourceRef,
+ );
+ const workflowSourceKind = WORKFLOW_SOURCE_KINDS[values.workflowSourceKind];
const compatibleEnvironments = environments
.filter(isCloneBasedEnvironment)
.sort((left, right) => left.id.localeCompare(right.id));
@@ -585,13 +594,13 @@ export function AutomationFormFields({
})}
className={`${INPUT_CLASS} font-mono`}
>
-
-
-
+ {Object.entries(WORKFLOW_SOURCE_KINDS).map(([kind, copy]) => (
+
+ ))}
{workflowSourceRefLabel(values.workflowSourceKind)}}
+ title={}
help={workflowSourceRefHelp(values.workflowSourceKind, workflowSourceRefValid)}
>
patch({ workflowSourceRef: e.target.value })}
- placeholder={workflowSourceRefPlaceholder(values.workflowSourceKind)}
+ placeholder={workflowSourceKind.placeholder}
autoComplete="off"
spellCheck={false}
className={`${INPUT_CLASS} font-mono`}
diff --git a/apps/fabro-web/app/lib/automation.ts b/apps/fabro-web/app/lib/automation.ts
index 74a7b4014..f90f75a22 100644
--- a/apps/fabro-web/app/lib/automation.ts
+++ b/apps/fabro-web/app/lib/automation.ts
@@ -36,3 +36,10 @@ export function hasEnabledApiTrigger(automation: Automation): boolean {
export function workflowSourceSummary(source: AutomationGitWorkflowSource): string {
return `${source.repo} · ${source.kind} ${source.ref}`;
}
+
+export const RUN_TARGET_CHECKOUT_LABEL = "run target checkout";
+
+/** Where an automation's workflow files come from, for display. */
+export function workflowSourceLabel(source: AutomationGitWorkflowSource | undefined): string {
+ return source ? workflowSourceSummary(source) : RUN_TARGET_CHECKOUT_LABEL;
+}
diff --git a/apps/fabro-web/app/routes/automation-detail.tsx b/apps/fabro-web/app/routes/automation-detail.tsx
index f7bb47818..289b2f632 100644
--- a/apps/fabro-web/app/routes/automation-detail.tsx
+++ b/apps/fabro-web/app/routes/automation-detail.tsx
@@ -24,7 +24,7 @@ import {
findApiTrigger,
findScheduleTrigger,
gitTarget,
- workflowSourceSummary,
+ workflowSourceLabel,
} from "../lib/automation";
import { useAutomation, useAutomationRuns } from "../lib/queries";
import { queryKeys } from "../lib/query-keys";
@@ -101,7 +101,6 @@ function AutomationHeader({ automation }: { automation: Automation }) {
const scheduleTrigger = findScheduleTrigger(automation);
const apiTrigger = findApiTrigger(automation);
const target = gitTarget(automation.target);
- const workflowSource = automation.workflow_source;
const canRun = apiTrigger?.enabled === true && automation.environment_id !== null;
async function onRun() {
@@ -158,9 +157,7 @@ function AutomationHeader({ automation }: { automation: Automation }) {
) : null}
- Workflow · {automation.workflow} · {workflowSource
- ? workflowSourceSummary(workflowSource)
- : "run target checkout"}
+ Workflow · {automation.workflow} · {workflowSourceLabel(automation.workflow_source)}
{automation.environment_id ?? (
diff --git a/apps/fabro-web/app/routes/automations.tsx b/apps/fabro-web/app/routes/automations.tsx
index 11a9c7d6b..afd426e9d 100644
--- a/apps/fabro-web/app/routes/automations.tsx
+++ b/apps/fabro-web/app/routes/automations.tsx
@@ -19,6 +19,7 @@ import type { Automation, AutomationListResponse } from "@qltysh/fabro-api-clien
import { Link, useNavigate } from "react-router";
import { ApiError, apiData, automationsApi } from "../lib/api-client";
import {
+ RUN_TARGET_CHECKOUT_LABEL,
UNSUPPORTED_TARGET_LABEL,
findScheduleTrigger,
gitTarget,
@@ -160,7 +161,7 @@ function AutomationCard({
- Workflow source · {automation.workflowSource ?? "run target checkout"}
+ Workflow source · {automation.workflowSource ?? RUN_TARGET_CHECKOUT_LABEL}
diff --git a/lib/apps/fabro-server/src/automation_materializer.rs b/lib/apps/fabro-server/src/automation_materializer.rs
index d16ccf879..dba8ff0ac 100644
--- a/lib/apps/fabro-server/src/automation_materializer.rs
+++ b/lib/apps/fabro-server/src/automation_materializer.rs
@@ -13,7 +13,7 @@ use tokio::{fs, task};
use crate::git_checkout::{
GitAuthConfig, GitCheckoutError, GitCheckoutSelector, GitRepoCache, WorktreePrepareInput,
- resolve_git_auth_config,
+ github_clone_url, resolve_git_auth_config,
};
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -60,23 +60,15 @@ pub(crate) enum RunMaterializeError {
#[source]
source: AutomationValidationError,
},
- #[error("failed to resolve automation target credentials")]
- TargetCredentials {
+ #[error("failed to resolve automation {role} credentials")]
+ Credentials {
+ role: CheckoutRole,
#[source]
source: anyhow::Error,
},
- #[error("failed to prepare automation target checkout")]
- TargetCheckout {
- #[source]
- source: GitCheckoutError,
- },
- #[error("failed to resolve automation workflow-source credentials")]
- WorkflowSourceCredentials {
- #[source]
- source: anyhow::Error,
- },
- #[error("failed to prepare automation workflow-source checkout")]
- WorkflowSourceCheckout {
+ #[error("failed to prepare automation {role} checkout")]
+ Checkout {
+ role: CheckoutRole,
#[source]
source: GitCheckoutError,
},
@@ -113,6 +105,15 @@ pub(crate) enum RunMaterializeError {
},
}
+/// Which repository a checkout serves; only the error message differs.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display)]
+pub(crate) enum CheckoutRole {
+ #[strum(serialize = "target")]
+ Target,
+ #[strum(serialize = "workflow-source")]
+ WorkflowSource,
+}
+
#[async_trait]
pub(crate) trait AutomationRunMaterializer: Send + Sync {
async fn materialize(
@@ -123,34 +124,43 @@ pub(crate) trait AutomationRunMaterializer: Send + Sync {
#[derive(Clone)]
pub(crate) struct ProductionAutomationRunMaterializer {
- credential_resolver: Arc,
- repo_cache: Arc,
- version_store: WorkflowVersionStore,
- #[cfg(test)]
- clone_urls: Arc>,
+ remote_resolver: Arc,
+ repo_cache: Arc,
+ version_store: WorkflowVersionStore,
+}
+
+/// Where to fetch a repository from and how to authenticate.
+#[derive(Clone)]
+struct GitRemote {
+ clone_url: String,
+ auth: Option,
}
#[async_trait]
-trait AutomationGitCredentialResolver: Send + Sync {
- async fn resolve(&self, repo: &GitHubRepositorySlug) -> anyhow::Result