Skip to content

Commit 8e2f492

Browse files
authored
Merge pull request #32 from BotCoder254/feat/log-chunk-sections
fix(sync): re-parse stored workflows after parser upgrades so detection lights up
2 parents 80a5598 + 02c0180 commit 8e2f492

5 files changed

Lines changed: 65 additions & 17 deletions

File tree

‎backend/src/db/workflows.rs‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,32 @@ use uuid::Uuid;
66
use crate::models::workflow::{WorkflowDetailRow, WorkflowJobRow, WorkflowSummaryRow};
77
use crate::services::workflow_parse::ParsedJob;
88

9-
/// blob shas of the workflows currently stored for a repository — sync
10-
/// compares against GitHub's listing to fetch only changed files.
11-
pub async fn blob_shas_for_repo(
9+
/// Per-path sync state of the workflows currently stored for a repository:
10+
/// blob sha (fetch only changed files) and the stamped parser version
11+
/// (re-parse stored files after a parser upgrade even when the sha is
12+
/// unchanged — otherwise new metadata never materializes). The `jsonb_typeof`
13+
/// guard reads legacy/hand-edited metadata as version 0, never an error.
14+
pub async fn sync_states_for_repo(
1215
pool: &PgPool,
1316
repository_id: Uuid,
14-
) -> sqlx::Result<HashMap<String, String>> {
15-
let rows: Vec<(String, String)> =
16-
sqlx::query_as("SELECT path, blob_sha FROM workflows WHERE repository_id = $1")
17-
.bind(repository_id)
18-
.fetch_all(pool)
19-
.await?;
20-
Ok(rows.into_iter().collect())
17+
) -> sqlx::Result<HashMap<String, (String, i64)>> {
18+
let rows: Vec<(String, String, i64)> = sqlx::query_as(
19+
r#"
20+
SELECT path, blob_sha,
21+
CASE WHEN jsonb_typeof(metadata->'parserVersion') = 'number'
22+
THEN floor((metadata->>'parserVersion')::numeric)::bigint
23+
ELSE 0 END
24+
FROM workflows
25+
WHERE repository_id = $1
26+
"#,
27+
)
28+
.bind(repository_id)
29+
.fetch_all(pool)
30+
.await?;
31+
Ok(rows
32+
.into_iter()
33+
.map(|(path, sha, version)| (path, (sha, version)))
34+
.collect())
2135
}
2236

2337
/// Upsert one workflow and replace its job rows. Runs inside the sync

‎backend/src/services/repo_sync.rs‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,22 @@ async fn run_sync(
135135
.take(MAX_WORKFLOW_FILES)
136136
.collect();
137137

138-
let known = db::workflows::blob_shas_for_repo(&state.pool, repository.id)
138+
let known = db::workflows::sync_states_for_repo(&state.pool, repository.id)
139139
.await
140140
.map_err(|e| fail("database error")(e.into()))?;
141141

142142
// Fetch + parse changed files outside the transaction; persist inside it.
143143
let mut parsed_files = Vec::new();
144144
let mut fetched = 0usize;
145145
for entry in &eligible {
146-
if known.get(&entry.path).map(String::as_str) == Some(entry.sha.as_str()) {
147-
continue; // unchanged
146+
// Skip only when the content is unchanged AND it was parsed by the
147+
// current parser — a parser upgrade re-parses stored files once so
148+
// new metadata (secretRefs/varRefs/environments) materializes for
149+
// workflows that never change on GitHub.
150+
if known.get(&entry.path).is_some_and(|(sha, version)| {
151+
sha == &entry.sha && *version == workflow_parse::PARSER_VERSION
152+
}) {
153+
continue;
148154
}
149155
if entry.size as usize > github_app::MAX_WORKFLOW_FILE_BYTES {
150156
// Oversized files become an errored catalog entry, not a fetch.
@@ -314,7 +320,9 @@ fn oversize_placeholder() -> workflow_parse::ParsedWorkflow {
314320
name: None,
315321
triggers: Vec::new(),
316322
jobs: Vec::new(),
317-
metadata: serde_json::json!({}),
323+
// Version-stamped so sync doesn't re-process the oversized file on
324+
// every run.
325+
metadata: serde_json::json!({ "parserVersion": workflow_parse::PARSER_VERSION }),
318326
diagnostics: vec![workflow_parse::Diagnostic {
319327
severity: workflow_parse::Severity::Error,
320328
message: "workflow file exceeds the size limit and was not parsed".into(),

‎backend/src/services/workflow_parse.rs‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ use serde_yaml_ng::{Mapping, Value};
1313

1414
use super::github_app::MAX_WORKFLOW_FILE_BYTES;
1515

16+
/// Stamped into every workflow's `metadata` JSONB. Bump whenever the parse
17+
/// output or metadata shape changes: repo sync re-parses any stored workflow
18+
/// whose stamped version is older, even when its blob sha is unchanged —
19+
/// otherwise new metadata (e.g. the detected-requirements ref arrays) would
20+
/// never materialize for files that don't change on GitHub.
21+
pub const PARSER_VERSION: i64 = 2;
22+
1623
/// Resource budgets: a workflow file that exceeds these is hostile or
1724
/// broken, not "large". They bound both memory and walk time.
1825
const MAX_NODES: usize = 20_000;
@@ -141,7 +148,9 @@ impl ParsedWorkflow {
141148
name: None,
142149
triggers: Vec::new(),
143150
jobs: Vec::new(),
144-
metadata: serde_json::json!({}),
151+
// Version-stamped even on failure so sync doesn't re-parse a
152+
// persistently broken file on every run.
153+
metadata: serde_json::json!({ "parserVersion": PARSER_VERSION }),
145154
diagnostics,
146155
}
147156
}
@@ -206,6 +215,7 @@ pub fn parse_and_validate(content: &str) -> ParsedWorkflow {
206215
validate_needs(&jobs, &mut diagnostics);
207216

208217
let metadata = serde_json::json!({
218+
"parserVersion": PARSER_VERSION,
209219
"permissions": get(root, "permissions").map(value_to_json),
210220
"concurrency": get(root, "concurrency").map(value_to_json),
211221
"envKeys": get(root, "env")
@@ -1106,4 +1116,20 @@ jobs:
11061116
assert_eq!(parsed.metadata["varRefs"], serde_json::json!([]));
11071117
assert_eq!(parsed.metadata["environments"], serde_json::json!([]));
11081118
}
1119+
1120+
#[test]
1121+
fn metadata_is_parser_version_stamped_on_success_and_failure() {
1122+
let parsed = parse_and_validate(
1123+
"on: push\njobs:\n a:\n runs-on: ubuntu-latest\n steps: []\n",
1124+
);
1125+
assert_eq!(
1126+
parsed.metadata["parserVersion"],
1127+
serde_json::json!(PARSER_VERSION)
1128+
);
1129+
let broken = parse_and_validate("{not yaml");
1130+
assert_eq!(
1131+
broken.metadata["parserVersion"],
1132+
serde_json::json!(PARSER_VERSION)
1133+
);
1134+
}
11091135
}

‎src/features/environments/pages/EnvironmentsPage.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export function EnvironmentsPage() {
164164
description={
165165
q
166166
? 'Nothing matches the current search. Clear it to see every environment.'
167-
: 'Create environments like production or staging, give each its own secrets, and reference them from workflow YAML with `environment: <name>`.'
167+
: 'Create environments like production or staging, give each its own secrets, and reference them from workflow YAML with `environment: <name>`. Environments referenced in YAML appear as detected entries after the repository’s next sync.'
168168
}
169169
action={
170170
!q ? (

‎src/features/secrets/pages/SecretsPage.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export function SecretsPage() {
158158
description={
159159
hasFilters
160160
? 'Nothing matches the current filters. Clear them to see every secret.'
161-
: 'Store deployment tokens, registry credentials, and API keys here — encrypted at rest, injected into jobs at run time, masked in logs.'
161+
: 'Store deployment tokens, registry credentials, and API keys here — encrypted at rest, injected into jobs at run time, masked in logs. GitHub never exposes existing secret values, so they can’t be imported: re-enter each value once and it stays encrypted here.'
162162
}
163163
action={
164164
!hasFilters ? (

0 commit comments

Comments
 (0)