@dotdotdash/afterhours 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  - **Dependency audit** — finds vulnerable and outdated packages, upgrades safe deps automatically, uses an LLM agent to fix any breakages
10
10
  - **Code optimization** — runs an LLM agent to find and apply code quality improvements, verifies each change with your build + test suite
11
- - **Deployment** — pushes Docker images, publishes npm packages, creates GitHub Releases, deploys to Vercel or Render
11
+ - **Deployment** — pushes Docker images, publishes npm packages, creates GitHub Releases, deploys to Vercel or Render, or publishes a directory to a git branch (e.g. GitHub Pages)
12
12
  - **Platform integrations** — GitHub, GitLab, Azure Repos, AWS CodeCommit, Google Cloud Source (PR/MR creation, auto-merge, commenting)
13
13
  - **Notifications** — PR comment, email, or webhook on every run
14
14
  - **Scheduler adapters** — GitHub Actions, Docker + cron, or bring your own
@@ -103,6 +103,24 @@ llm:
103
103
 
104
104
  The Copilot account is fully independent from `platform` (repo access) — `apiKeyEnv` here can point to a token for a different GitHub account than `GITHUB_TOKEN`. This lets an org member's Copilot seat drive automated work against a repo (e.g. a client-owned repo) where only a separate PAT has push/PR access. Note that this relies on GitHub's internal Copilot chat-completions API (the same mechanism used by third-party editor integrations); confirm it's compatible with your Copilot license terms before enabling it for unattended/agentic use.
105
105
 
106
+ ### Branch-based deployment (`git-branch` target)
107
+
108
+ Publish a directory to a branch that some other host watches (e.g. `gh-pages` for GitHub Pages):
109
+
110
+ ```yaml
111
+ tasks:
112
+ deployment:
113
+ enabled: true
114
+ targets:
115
+ - type: git-branch
116
+ branch: gh-pages
117
+ publishDir: dist # relative to repo root; defaults to the whole repo
118
+ buildCmd: npm run build # optional; omit or set to 'none' to skip
119
+ remote: origin # defaults to 'origin'
120
+ ```
121
+
122
+ This target builds the project (if `buildCmd` is set), checks out the target branch in an isolated `git worktree` (created fresh as an orphan branch if it doesn't exist remotely yet), replaces its contents with `publishDir`, commits, and pushes — without disturbing the main working tree used by the rest of the run. It uses whatever git credentials are already configured for the repo (the same remote/auth the orchestrator uses to push its own branches), so no extra env vars are required.
123
+
106
124
  ---
107
125
 
108
126
  ## Environment variables
package/dist/index.js CHANGED
@@ -2679,6 +2679,73 @@ var RenderTarget = class {
2679
2679
  return `render deploy service ${this.config.serviceIdEnv}`;
2680
2680
  }
2681
2681
  };
2682
+ var GitBranchTarget = class {
2683
+ constructor(config) {
2684
+ this.config = config;
2685
+ }
2686
+ config;
2687
+ type = "git-branch";
2688
+ validate() {
2689
+ if (!this.config.branch) throw new Error('git-branch target requires a "branch"');
2690
+ }
2691
+ async deploy(ctx) {
2692
+ const remote = this.config.remote ?? "origin";
2693
+ const branch = this.config.branch;
2694
+ const publishDir = this.config.publishDir ?? ".";
2695
+ const worktreeDir = `.afterhours-deploy-${branch.replace(/[^a-zA-Z0-9._-]/g, "-")}`;
2696
+ const message = this.config.commitMessage ?? `Deploy ${branch}${ctx.gitSha ? ` (${ctx.gitSha})` : ""}`;
2697
+ if (this.config.buildCmd && this.config.buildCmd !== "none") {
2698
+ const build = await ctx.sandbox.run(this.config.buildCmd, { cwd: ctx.repoRoot, timeoutSec: 600 });
2699
+ if (build.code !== 0) return { targetType: "git-branch", status: "failed", error: build.stderr || "Build command failed" };
2700
+ }
2701
+ try {
2702
+ await ctx.sandbox.run(`git worktree remove --force "${worktreeDir}"`, { cwd: ctx.repoRoot });
2703
+ await ctx.sandbox.run(`rm -rf "${worktreeDir}"`, { cwd: ctx.repoRoot });
2704
+ await ctx.sandbox.run(`git worktree prune`, { cwd: ctx.repoRoot });
2705
+ await ctx.sandbox.run(`git fetch ${remote} ${branch}`, { cwd: ctx.repoRoot });
2706
+ const remoteCheck = await ctx.sandbox.run(`git ls-remote --exit-code --heads ${remote} ${branch}`, { cwd: ctx.repoRoot });
2707
+ const branchExists = remoteCheck.code === 0;
2708
+ const add = branchExists ? await ctx.sandbox.run(`git worktree add -B ${branch} "${worktreeDir}" ${remote}/${branch}`, { cwd: ctx.repoRoot, timeoutSec: 120 }) : await ctx.sandbox.run(`git worktree add --detach "${worktreeDir}"`, { cwd: ctx.repoRoot, timeoutSec: 120 });
2709
+ if (add.code !== 0) {
2710
+ return { targetType: "git-branch", status: "failed", error: add.stderr || "git worktree add failed" };
2711
+ }
2712
+ if (!branchExists) {
2713
+ const orphan = await ctx.sandbox.run(`git -C "${worktreeDir}" checkout --orphan ${branch}`, { cwd: ctx.repoRoot, timeoutSec: 60 });
2714
+ if (orphan.code !== 0) {
2715
+ return { targetType: "git-branch", status: "failed", error: orphan.stderr || "Failed to create orphan branch" };
2716
+ }
2717
+ }
2718
+ await ctx.sandbox.run(`git -C "${worktreeDir}" rm -rf . --quiet`, { cwd: ctx.repoRoot, timeoutSec: 60 });
2719
+ const copy = await ctx.sandbox.run(
2720
+ `find "${publishDir}" -mindepth 1 -maxdepth 1 -not -name '.git' -not -name "${worktreeDir}" -exec cp -r {} "${worktreeDir}/" \\;`,
2721
+ { cwd: ctx.repoRoot, timeoutSec: 120 }
2722
+ );
2723
+ if (copy.code !== 0) {
2724
+ return { targetType: "git-branch", status: "failed", error: copy.stderr || "Failed to copy publish directory contents" };
2725
+ }
2726
+ await ctx.sandbox.run(`git -C "${worktreeDir}" add -A`, { cwd: ctx.repoRoot, timeoutSec: 60 });
2727
+ const diffCheck = await ctx.sandbox.run(`git -C "${worktreeDir}" diff --cached --quiet`, { cwd: ctx.repoRoot, timeoutSec: 30 });
2728
+ if (diffCheck.code === 0) {
2729
+ return { targetType: "git-branch", status: "skipped" };
2730
+ }
2731
+ const commitRes = await ctx.sandbox.run(`git -C "${worktreeDir}" commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: ctx.repoRoot, timeoutSec: 60 });
2732
+ if (commitRes.code !== 0) {
2733
+ return { targetType: "git-branch", status: "failed", error: commitRes.stderr || "git commit failed" };
2734
+ }
2735
+ const pushRes = await ctx.sandbox.run(`git -C "${worktreeDir}" push ${remote} HEAD:${branch}`, { cwd: ctx.repoRoot, timeoutSec: 120 });
2736
+ if (pushRes.code !== 0) {
2737
+ return { targetType: "git-branch", status: "failed", error: pushRes.stderr || "git push failed" };
2738
+ }
2739
+ return { targetType: "git-branch", status: "succeeded", url: `${remote}/${branch}` };
2740
+ } finally {
2741
+ await ctx.sandbox.run(`git worktree remove --force "${worktreeDir}"`, { cwd: ctx.repoRoot }).catch(() => void 0);
2742
+ await ctx.sandbox.run(`rm -rf "${worktreeDir}"`, { cwd: ctx.repoRoot }).catch(() => void 0);
2743
+ }
2744
+ }
2745
+ describe() {
2746
+ return `push ${this.config.publishDir ?? "."} to branch "${this.config.branch}"`;
2747
+ }
2748
+ };
2682
2749
  var CloudStubTarget = class {
2683
2750
  constructor(config) {
2684
2751
  this.config = config;
@@ -2721,6 +2788,8 @@ function createDeployTarget(config, ctx) {
2721
2788
  return new VercelTarget(config);
2722
2789
  case "render":
2723
2790
  return new RenderTarget(config);
2791
+ case "git-branch":
2792
+ return new GitBranchTarget(config);
2724
2793
  case "aws":
2725
2794
  case "azure":
2726
2795
  case "gcp":
@@ -2759,7 +2828,7 @@ var DeploymentTask = class {
2759
2828
  anyFailed = true;
2760
2829
  continue;
2761
2830
  }
2762
- const result = await target.deploy({ sandbox: ctx.sandbox, repoRoot: ctx.repoRoot });
2831
+ const result = await target.deploy({ sandbox: ctx.sandbox, repoRoot: ctx.repoRoot, gitSha: await getHeadSha(ctx.repoRoot) });
2763
2832
  results.push(result);
2764
2833
  report.deployments.push(result);
2765
2834
  if (result.status === "failed") anyFailed = true;
@@ -2772,6 +2841,13 @@ var DeploymentTask = class {
2772
2841
  return { status: succeeded > 0 ? "changed" : "noop", summary: `Deployment: ${succeeded} target(s) deployed.` };
2773
2842
  }
2774
2843
  };
2844
+ async function getHeadSha(repoRoot) {
2845
+ try {
2846
+ return (await createGit(repoRoot).revparse(["--short", "HEAD"])).trim();
2847
+ } catch {
2848
+ return void 0;
2849
+ }
2850
+ }
2775
2851
 
2776
2852
  // src/cli/run.ts
2777
2853
  function registerRun(program) {
@@ -2824,7 +2900,8 @@ function registerReport(program) {
2824
2900
 
2825
2901
  // src/cli/init.ts
2826
2902
  import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync, appendFileSync } from "fs";
2827
- import { join as join6 } from "path";
2903
+ import { join as join6, dirname as dirname2 } from "path";
2904
+ import { fileURLToPath } from "url";
2828
2905
  function registerInit(program) {
2829
2906
  program.command("init").description("Scaffold afterhours config and an optional scheduler adapter in this repo").option("--force", "Overwrite existing .afterhours/config.yml").option("--scheduler <type>", "Scheduler adapter to install: github | docker-cron | none", "none").action(async (opts) => {
2830
2907
  const cwd = process.cwd();
@@ -2892,8 +2969,9 @@ ${line}`);
2892
2969
  });
2893
2970
  }
2894
2971
  function getTemplateDir() {
2972
+ const moduleDir = dirname2(fileURLToPath(import.meta.url));
2895
2973
  const candidates = [
2896
- join6(new URL(import.meta.url).pathname, "..", "..", "..", "templates"),
2974
+ join6(moduleDir, "..", "templates"),
2897
2975
  join6(process.cwd(), "templates")
2898
2976
  ];
2899
2977
  for (const c of candidates) {
@@ -2905,7 +2983,7 @@ function getTemplateDir() {
2905
2983
  // src/cli/main.ts
2906
2984
  function main() {
2907
2985
  const program = new Command();
2908
- program.name("afterhours").description("afterhours CLI").version("0.1.0");
2986
+ program.name("afterhours").description("afterhours CLI").version("0.1.2");
2909
2987
  registerInit(program);
2910
2988
  registerRun(program);
2911
2989
  program.command("audit").action(() => console.log("<audit>: not implemented yet"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotdash/afterhours",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Autonomous schedulable system that maintains web codebases",
6
6
  "keywords": [
@@ -66,6 +66,11 @@ tasks:
66
66
  # - type: github-release
67
67
  # tagFrom: package.json
68
68
  # generateNotes: true
69
+ # - type: git-branch # push a directory to a branch a host watches (e.g. GitHub Pages)
70
+ # branch: gh-pages
71
+ # publishDir: dist # relative to repo root; defaults to the whole repo
72
+ # buildCmd: npm run build
73
+ # remote: origin
69
74
  issueTriage:
70
75
  enabled: false # requires a platform token with issue read/write access (e.g. GITHUB_TOKEN)
71
76
  allowedAuthors: # usernames allowed to trigger automated issue resolution, or ["*"] for anyone