(argv: list[str])
| 1132 | |
| 1133 | |
| 1134 | def main(argv: list[str]) -> int: |
| 1135 | args = parse_args(argv) |
| 1136 | today = dt.date.fromisoformat(args.today) if args.today else dt.date.today() |
| 1137 | floor = dt.date.fromisoformat(args.floor) |
| 1138 | requested = [p.strip() for p in args.platforms.split(",") if p.strip()] |
| 1139 | blog_dir = Path(args.blog_dir) |
| 1140 | state_file = Path(args.state_file) |
| 1141 | |
| 1142 | unknown = [p for p in requested if p not in ADAPTERS] |
| 1143 | if unknown: |
| 1144 | print(f"Unknown platform(s): {unknown}. Known: {sorted(ADAPTERS)}", file=sys.stderr) |
| 1145 | return 1 |
| 1146 | |
| 1147 | adapters: list[Any] = [] |
| 1148 | skipped: list[str] = [] |
| 1149 | for name in requested: |
| 1150 | adapter = ADAPTERS[name]() |
| 1151 | if args.dry_run or args.validate_only or adapter.is_configured(): |
| 1152 | adapters.append(adapter) |
| 1153 | else: |
| 1154 | skipped.append(name) |
| 1155 | |
| 1156 | if skipped: |
| 1157 | if _running_in_ci(): |
| 1158 | # Expected in CI — session secrets are intentionally not exposed |
| 1159 | # there. Keep it to a quiet one-liner per platform. |
| 1160 | for name in skipped: |
| 1161 | print(f"[{name}] credentials not configured; skipping platform (expected in CI).") |
| 1162 | else: |
| 1163 | # Local run: an unconfigured platform almost always means a |
| 1164 | # forgotten/expired session. Make it impossible to miss so a |
| 1165 | # platform can't silently drop out of the rotation again. |
| 1166 | print("!" * 72, file=sys.stderr) |
| 1167 | print(f"WARNING: skipping {len(skipped)} unconfigured platform(s) on a LOCAL run:", |
| 1168 | file=sys.stderr) |
| 1169 | for name in skipped: |
| 1170 | hint = SETUP_HINTS.get(name, "set its credentials") |
| 1171 | print(f" - {name}: not configured — {hint}", file=sys.stderr) |
| 1172 | print("These will NOT be syndicated. If that is not intended, configure them and re-run.", |
| 1173 | file=sys.stderr) |
| 1174 | print("!" * 72, file=sys.stderr) |
| 1175 | |
| 1176 | if not adapters: |
| 1177 | print("No browser platforms are configured; nothing to do.") |
| 1178 | return 0 |
| 1179 | |
| 1180 | posts = discover_posts(blog_dir) |
| 1181 | state = State.load(state_file) |
| 1182 | platform_names = [a.name for a in adapters] |
| 1183 | platform_filters = {a.name: a.accepts for a in adapters if hasattr(a, "accepts")} |
| 1184 | candidate = select_candidate(posts, state, platform_names, today, floor, args.min_age_days, |
| 1185 | platform_filters=platform_filters) |
| 1186 | if candidate is None and not args.validate_only: |
| 1187 | print("No syndication candidate found today.") |
| 1188 | return 0 |
| 1189 | if candidate is None and args.validate_only: |
| 1190 | # In validate-only mode, fall back to the newest post so we can still |
| 1191 | # verify selectors even when nothing is technically due. |
no test coverage detected