@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 +19 -1
- package/dist/index.js +82 -4
- package/package.json +1 -1
- package/templates/config.yml +5 -0
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(
|
|
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.
|
|
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
package/templates/config.yml
CHANGED
|
@@ -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
|