Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,15 @@ Dashboard Mode uses the same action with `dashboard: true`:
prompt_detail: compact
```

The action resolves the published GitHub Pages URL automatically, including private and Enterprise Pages sites served from `*.pages.github.io` domains. Set `pages_base_url` only when the resolved URL needs an explicit override, for example behind a custom proxy:

```yaml
- uses: wasabeef/AgentNote@v1
with:
dashboard: true
pages_base_url: https://pages.example.com/apps/
```

### Dashboard Data

For most repositories, you do not need to hand-write the workflow. Generate it with `init`:
Expand Down
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ inputs:
description: "PR Report prompt detail: compact or full (default: compact)"
required: false
default: "compact"
pages_base_url:
description: "Override the GitHub Pages base URL used for Dashboard links and asset paths (default: resolved from the Pages API, falling back to https://<owner>.github.io/<repo>/)"
required: false
default: ""
outputs:
overall_ai_ratio:
description: "PR-wide AI ratio (0-100)"
Expand Down Expand Up @@ -62,6 +66,7 @@ runs:
INPUT_BASE: ${{ inputs.base }}
INPUT_PR_OUTPUT: ${{ inputs.pr_output }}
INPUT_PROMPT_DETAIL: ${{ inputs.prompt_detail }}
INPUT_PAGES_BASE_URL: ${{ inputs.pages_base_url }}
run: node "$AGENT_NOTE_ACTION_PATH/packages/pr-report/dist/index.js"

- name: Detect Dashboard checkout
Expand Down Expand Up @@ -103,6 +108,8 @@ runs:
shell: bash
env:
DASHBOARD_ACTION_PATH: ${{ github.action_path }}/packages/dashboard
GH_TOKEN: ${{ github.token }}
PAGES_BASE_URL: ${{ inputs.pages_base_url }}
run: npm --prefix "$DASHBOARD_ACTION_PATH" run workflow:resolve-pages-target

- name: Restore Dashboard notes from gh-pages
Expand Down Expand Up @@ -140,6 +147,7 @@ runs:
NOTES_DIR: ${{ steps.dashboard_paths.outputs.notes_dir }}
PAGES_DIR: ${{ steps.dashboard_paths.outputs.pages_dir }}
DASHBOARD_PAGES_MODE: ${{ steps.dashboard_paths.outputs.publish_mode }}
PAGES_BASE_URL: ${{ steps.dashboard_paths.outputs.pages_base_url }}
PUBLIC_REPO: ${{ github.repository }}
run: npm --prefix "$DASHBOARD_ACTION_PATH" run workflow:build-pages

Expand Down
16 changes: 14 additions & 2 deletions packages/cli/dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -6277,7 +6277,19 @@ ${section}${after}`;

${section}`;
}
function inferDashboardUrl(repoUrl, prNumber) {
function parseUrlOrNull(value) {
try {
return new URL(value);
} catch {
return null;
}
}
function inferDashboardUrl(repoUrl, prNumber, pagesBaseUrl) {
const parsedBase = pagesBaseUrl ? parseUrlOrNull(pagesBaseUrl) : null;
if (parsedBase) {
const basePath = parsedBase.pathname.replace(/\/+$/, "");
return appendPrNumber(`${parsedBase.origin}${basePath}/dashboard/`, prNumber);
}
if (!repoUrl) return null;
const normalized = repoUrl.replace(/\.git$/, "");
const match = normalized.match(GITHUB_REPOSITORY_URL_PATTERN);
Expand Down Expand Up @@ -6549,7 +6561,7 @@ async function collectReport(base, headRef = "HEAD", opts = {}) {
const hasDashboardWorkflow = existsSync14(
join14(repoRoot3, ".github", "workflows", "agentnote-dashboard.yml")
);
const dashboardUrl = hasDashboardWorkflow ? inferDashboardUrl(repoUrl, opts.dashboardPrNumber) : null;
const dashboardUrl = hasDashboardWorkflow ? inferDashboardUrl(repoUrl, opts.dashboardPrNumber, opts.pagesBaseUrl) : null;
return {
base,
head,
Expand Down
12 changes: 4 additions & 8 deletions packages/dashboard/workflow/build-pages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { PUBLISH_MODE_INTEGRATED } from "./resolve-pages-target.mjs";
import { derivePagesPaths, PUBLISH_MODE_INTEGRATED } from "./resolve-pages-target.mjs";

const DASHBOARD_SUBDIRECTORY = "dashboard";
const DEFAULT_DASHBOARD_NOTES_DIR = ".agentnote-dashboard-notes";
Expand All @@ -12,18 +12,18 @@ const DIST_DIR_NAME = "dist";
const ENV_DASHBOARD_PAGES_MODE = "DASHBOARD_PAGES_MODE";
const ENV_GITHUB_REPOSITORY = "GITHUB_REPOSITORY";
const ENV_NOTES_DIR = "NOTES_DIR";
const ENV_PAGES_BASE_URL = "PAGES_BASE_URL";
const ENV_PAGES_DIR = "PAGES_DIR";
const ENV_PUBLIC_REPO = "PUBLIC_REPO";
const GITHUB_PAGES_HOST_SUFFIX = ".github.io";
const NOTES_DIR_NAME = "notes";
const ORIGINAL_NOTES_DIR_NAME = "original-notes";
const PUBLIC_DIR_NAME = "public";
const TEMP_DIR_PREFIX = "agentnote-dashboard-build-";
const notesDir = process.env[ENV_NOTES_DIR] || DEFAULT_DASHBOARD_NOTES_DIR;
const pagesDir = process.env[ENV_PAGES_DIR] || DEFAULT_PAGES_DIR;
const pagesMode = process.env[ENV_DASHBOARD_PAGES_MODE] || DEFAULT_PAGES_MODE;
const pagesBaseUrl = process.env[ENV_PAGES_BASE_URL] || "";
const repository = process.env[ENV_PUBLIC_REPO] || process.env[ENV_GITHUB_REPOSITORY] || "";
const [repositoryOwner = "", repositoryName = ""] = repository.split("/");

const dashboardDir = process.cwd();
const publicNotesDir = join(dashboardDir, PUBLIC_DIR_NAME, NOTES_DIR_NAME);
Expand All @@ -50,11 +50,7 @@ if (existsSync(publicNotesDir)) {
copyDirectoryContents(publicNotesDir, originalNotesDir);
}

const site = `https://${repositoryOwner}.github.io`;
const base =
repositoryName === `${repositoryOwner}${GITHUB_PAGES_HOST_SUFFIX}`
? `/${DASHBOARD_SUBDIRECTORY}`
: `/${repositoryName}/${DASHBOARD_SUBDIRECTORY}`;
const { site, base } = derivePagesPaths({ pagesBaseUrl, repository });

try {
// Astro reads dashboard notes from public/notes only during the static build.
Expand Down
92 changes: 90 additions & 2 deletions packages/dashboard/workflow/resolve-pages-target.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ export const PUBLISH_MODE_STANDALONE = "standalone";

const ACTIONS_FALSE = "false";
const ACTIONS_TRUE = "true";
const DASHBOARD_SUBDIRECTORY = "dashboard";
const DEFAULT_DASHBOARD_NOTES_DIR = ".agentnote-dashboard-notes";
const DEFAULT_GITHUB_API_URL = "https://api.github.com";
const DEFAULT_PAGES_ARTIFACT_PATH = "_site";
const DEFAULT_STANDALONE_PAGES_DIR = ".agentnote-pages";
const ENV_GH_TOKEN = "GH_TOKEN";
const ENV_GITHUB_API_URL = "GITHUB_API_URL";
const ENV_GITHUB_JOB = "GITHUB_JOB";
const ENV_GITHUB_OUTPUT = "GITHUB_OUTPUT";
const ENV_GITHUB_REPOSITORY = "GITHUB_REPOSITORY";
const ENV_GITHUB_WORKFLOW_REF = "GITHUB_WORKFLOW_REF";
const ENV_GITHUB_WORKSPACE = "GITHUB_WORKSPACE";
const ENV_PAGES_BASE_URL = "PAGES_BASE_URL";
const GITHUB_PAGES_HOST_SUFFIX = ".github.io";
const GITHUB_WORKFLOWS_DIR = ".github/workflows/";
const PAGES_API_TIMEOUT_MS = 10_000;
const REASON_DYNAMIC_PATH = "dynamic-path";
const REASON_OTHER_JOB = "other-job";
const REASON_OTHER_WORKFLOW = "other-workflow";
Expand Down Expand Up @@ -45,6 +52,78 @@ function leadingSpaces(line) {
return line.match(/^\s*/)?.[0].length ?? 0;
}

function parseUrlOrNull(value) {
try {
return new URL(value);
} catch {
return null;
}
}

/**
* Resolve the real GitHub Pages base URL for the current repository.
*
* Private and Enterprise Pages sites are served from obfuscated domains at the
* root path, so the URL cannot be derived from the repository name alone. The
* explicit override wins, then the Pages API, and callers fall back to the
* repository-name heuristic when neither is available.
*/
export async function resolvePagesBaseUrl({
override = process.env[ENV_PAGES_BASE_URL] || "",
repository = process.env[ENV_GITHUB_REPOSITORY] || "",
token = process.env[ENV_GH_TOKEN] || "",
apiUrl = process.env[ENV_GITHUB_API_URL] || DEFAULT_GITHUB_API_URL,
fetcher = fetch,
} = {}) {
// Re-serializing through URL both validates the value and strips characters
// that would corrupt the GITHUB_OUTPUT file, such as newlines.
const parsedOverride = override ? parseUrlOrNull(override) : null;
if (parsedOverride) return parsedOverride.toString();
if (override) console.log(`Ignoring invalid pages_base_url override: ${override}`);

if (!repository || !token) return null;

try {
const response = await fetcher(`${apiUrl}/repos/${repository}/pages`, {
headers: {
accept: "application/vnd.github+json",
authorization: `Bearer ${token}`,
},
signal: AbortSignal.timeout(PAGES_API_TIMEOUT_MS),
});
if (!response.ok) return null;
const pages = await response.json();
const htmlUrl = typeof pages?.html_url === "string" ? pages.html_url : "";
return htmlUrl ? (parseUrlOrNull(htmlUrl)?.toString() ?? null) : null;
} catch {
// Pages metadata is an enhancement; resolution failures fall back to the heuristic.
return null;
}
}

/**
* Derive the Astro site origin and dashboard base path.
*
* A resolved Pages URL takes precedence; otherwise the canonical
* `https://<owner>.github.io/<repo>/` layout is assumed.
*/
export function derivePagesPaths({ pagesBaseUrl = "", repository = "" } = {}) {
const parsed = pagesBaseUrl ? parseUrlOrNull(pagesBaseUrl) : null;
if (parsed) {
const basePath = parsed.pathname.replace(/\/+$/, "");
return { site: parsed.origin, base: `${basePath}/${DASHBOARD_SUBDIRECTORY}` };
}

const [owner = "", name = ""] = repository.split("/");
return {
site: `https://${owner}${GITHUB_PAGES_HOST_SUFFIX}`,
base:
name === `${owner}${GITHUB_PAGES_HOST_SUFFIX}`
? `/${DASHBOARD_SUBDIRECTORY}`
: `/${name}/${DASHBOARD_SUBDIRECTORY}`,
};
}

/**
* Resolve the caller workflow path from GitHub Actions metadata.
*
Expand Down Expand Up @@ -301,7 +380,7 @@ function readOtherWorkflowTexts(currentWorkflowPath) {
.filter(Boolean);
}

function main() {
async function main() {
const workflowPath = resolveWorkflowPath();
const workflowText = workflowPath && existsSync(workflowPath)
? readFileSync(workflowPath, TEXT_ENCODING)
Expand All @@ -311,6 +390,14 @@ function main() {
otherWorkflowTexts: readOtherWorkflowTexts(workflowPath),
workspaceDir: workspace,
});
const pagesBaseUrl = await resolvePagesBaseUrl();
if (pagesBaseUrl) {
console.log(`Resolved GitHub Pages base URL: ${pagesBaseUrl}`);
} else {
console.log(
"GitHub Pages base URL is not resolvable; falling back to the repository-name layout.",
);
}

if (target.publishMode === PUBLISH_MODE_INTEGRATED) {
console.log(`Detected existing Pages artifact path: ${target.pagesDir}`);
Expand All @@ -330,8 +417,9 @@ function main() {
setOutput("internal_upload", target.internalUpload);
setOutput("can_build", target.canBuild);
setOutput("skip_reason", target.reason);
setOutput("pages_base_url", pagesBaseUrl ?? "");
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
await main();
}
Loading
Loading