foxplug 1.0.0 → 1.0.1
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/LICENSE +21 -21
- package/README.md +86 -86
- package/foxplug.js +178 -178
- package/package.json +13 -13
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 FoxPlug
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FoxPlug
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
# foxplug
|
|
2
|
-
|
|
3
|
-
Every release or push becomes a changelog entry and ready-to-post launch updates, waiting in FoxPlug for you to approve. For GitLab CI, Bitbucket Pipelines, CircleCI, any other CI, or your terminal. On GitHub, use the [GitHub Action](https://github.com/marketplace/actions/foxplug-changelog-and-launch-posts) instead.
|
|
4
|
-
|
|
5
|
-
A live example: [FoxPlug's own changelog](https://foxplug.com/changelog/foxplug/?utm_source=npm&utm_medium=readme&utm_campaign=foxplug_npm), written by FoxPlug from its own commits.
|
|
6
|
-
|
|
7
|
-
Free for one project. Plans: [foxplug.com/pricing](https://foxplug.com/pricing?utm_source=npm&utm_medium=readme&utm_campaign=foxplug_npm).
|
|
8
|
-
|
|
9
|
-
## Set it up
|
|
10
|
-
|
|
11
|
-
1. **[Get your token](https://foxplug.com/app/?connect=github-action&utm_source=npm&utm_medium=readme&utm_campaign=foxplug_npm)**. This opens the token row of your FoxPlug project (you sign up or sign in first if you need to). Press **Create a token**. It is shown once.
|
|
12
|
-
2. Save it in your CI as a secret (masked) variable named `FOXPLUG_TOKEN`.
|
|
13
|
-
3. Add one step after your build:
|
|
14
|
-
|
|
15
|
-
**GitLab CI** (`.gitlab-ci.yml`)
|
|
16
|
-
|
|
17
|
-
```yaml
|
|
18
|
-
foxplug:
|
|
19
|
-
image: node:22-alpine
|
|
20
|
-
before_script: [apk add --no-cache git]
|
|
21
|
-
script: [npx -y foxplug@1]
|
|
22
|
-
rules:
|
|
23
|
-
- if: $CI_COMMIT_TAG
|
|
24
|
-
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
**Bitbucket Pipelines** (`bitbucket-pipelines.yml`)
|
|
28
|
-
|
|
29
|
-
```yaml
|
|
30
|
-
pipelines:
|
|
31
|
-
branches:
|
|
32
|
-
main:
|
|
33
|
-
- step: { name: FoxPlug, image: node:22, script: [npx -y foxplug@1] }
|
|
34
|
-
tags:
|
|
35
|
-
'*':
|
|
36
|
-
- step: { name: FoxPlug, image: node:22, script: [npx -y foxplug@1] }
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
**CircleCI** (a step in any job that has your code checked out and Node 18 or newer)
|
|
40
|
-
|
|
41
|
-
```yaml
|
|
42
|
-
- run: npx -y foxplug@1
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
**Your terminal**, from the repository's folder:
|
|
46
|
-
|
|
47
|
-
```sh
|
|
48
|
-
FOXPLUG_TOKEN=... npx foxplug # today's commits
|
|
49
|
-
FOXPLUG_TOKEN=... npx foxplug --release v1.4.0
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
That's it. The next release shows up in FoxPlug as a draft and a changelog entry, waiting for you. Commits are gathered by day: a single commit on its own waits until the next push that day joins it.
|
|
53
|
-
|
|
54
|
-
What it looks like once you publish:
|
|
55
|
-
|
|
56
|
-
[](https://foxplug.com/changelog/foxplug/?utm_source=npm&utm_medium=readme&utm_campaign=foxplug_npm)
|
|
57
|
-
|
|
58
|
-
## What it sends
|
|
59
|
-
|
|
60
|
-
- **On a release tag** (found automatically when GitLab, Bitbucket, CircleCI or GitHub builds a tag, or passed with `--release`): the tag, its name, its notes and its link. The notes are the commit subjects since the previous tag, or a file you pass with `--notes`.
|
|
61
|
-
- **Otherwise**: the subject line of each commit in this push (GitLab gives the push's range), or of today's commits.
|
|
62
|
-
- FoxPlug writes a changelog entry and launch posts from it, as drafts. **Nothing is posted until you approve it.**
|
|
63
|
-
- The command prints in one sentence what happened, for example that a draft is waiting.
|
|
64
|
-
|
|
65
|
-
**Nothing else leaves the machine: no source code, no secrets, no environment.** `npx foxplug --dry-run` prints exactly what would be sent. The command is one file with no dependencies, [foxplug.js](https://github.com/OsakaSaul/foxplug-changelog-action/blob/main/cli/foxplug.js).
|
|
66
|
-
|
|
67
|
-
## Options
|
|
68
|
-
|
|
69
|
-
| Option | What it is |
|
|
70
|
-
| --- | --- |
|
|
71
|
-
| `--release <tag>` | Send this tag as a release. |
|
|
72
|
-
| `--notes <file>` | Release notes from a file. |
|
|
73
|
-
| `--since <ref>` | Send the commits after this ref. |
|
|
74
|
-
| `--repo <owner/name>` | The repository name, if it cannot be read from git or CI. |
|
|
75
|
-
| `--project <name>` | Your FoxPlug project name, as a check that the token is the one you meant. |
|
|
76
|
-
| `--dry-run` | Print what would be sent; send nothing. |
|
|
77
|
-
|
|
78
|
-
A wrong or revoked token stops the step with a message. FoxPlug being unreachable never fails your build.
|
|
79
|
-
|
|
80
|
-
## Revoking
|
|
81
|
-
|
|
82
|
-
Revoke the token in your FoxPlug project at any time; the next run then stops with a message saying so.
|
|
83
|
-
|
|
84
|
-
## Licence
|
|
85
|
-
|
|
86
|
-
MIT
|
|
1
|
+
# foxplug
|
|
2
|
+
|
|
3
|
+
Every release or push becomes a changelog entry and ready-to-post launch updates, waiting in FoxPlug for you to approve. For GitLab CI, Bitbucket Pipelines, CircleCI, any other CI, or your terminal. On GitHub, use the [GitHub Action](https://github.com/marketplace/actions/foxplug-changelog-and-launch-posts) instead.
|
|
4
|
+
|
|
5
|
+
A live example: [FoxPlug's own changelog](https://foxplug.com/changelog/foxplug/?utm_source=npm&utm_medium=readme&utm_campaign=foxplug_npm), written by FoxPlug from its own commits.
|
|
6
|
+
|
|
7
|
+
Free for one project. Plans: [foxplug.com/pricing](https://foxplug.com/pricing?utm_source=npm&utm_medium=readme&utm_campaign=foxplug_npm).
|
|
8
|
+
|
|
9
|
+
## Set it up
|
|
10
|
+
|
|
11
|
+
1. **[Get your token](https://foxplug.com/app/?connect=github-action&utm_source=npm&utm_medium=readme&utm_campaign=foxplug_npm)**. This opens the token row of your FoxPlug project (you sign up or sign in first if you need to). Press **Create a token**. It is shown once.
|
|
12
|
+
2. Save it in your CI as a secret (masked) variable named `FOXPLUG_TOKEN`.
|
|
13
|
+
3. Add one step after your build:
|
|
14
|
+
|
|
15
|
+
**GitLab CI** (`.gitlab-ci.yml`)
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
foxplug:
|
|
19
|
+
image: node:22-alpine
|
|
20
|
+
before_script: [apk add --no-cache git]
|
|
21
|
+
script: [npx -y foxplug@1]
|
|
22
|
+
rules:
|
|
23
|
+
- if: $CI_COMMIT_TAG
|
|
24
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Bitbucket Pipelines** (`bitbucket-pipelines.yml`)
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
pipelines:
|
|
31
|
+
branches:
|
|
32
|
+
main:
|
|
33
|
+
- step: { name: FoxPlug, image: node:22, script: [npx -y foxplug@1] }
|
|
34
|
+
tags:
|
|
35
|
+
'*':
|
|
36
|
+
- step: { name: FoxPlug, image: node:22, script: [npx -y foxplug@1] }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**CircleCI** (a step in any job that has your code checked out and Node 18 or newer)
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
- run: npx -y foxplug@1
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Your terminal**, from the repository's folder:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
FOXPLUG_TOKEN=... npx foxplug # today's commits
|
|
49
|
+
FOXPLUG_TOKEN=... npx foxplug --release v1.4.0
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That's it. The next release shows up in FoxPlug as a draft and a changelog entry, waiting for you. Commits are gathered by day: a single commit on its own waits until the next push that day joins it.
|
|
53
|
+
|
|
54
|
+
What it looks like once you publish:
|
|
55
|
+
|
|
56
|
+
[](https://foxplug.com/changelog/foxplug/?utm_source=npm&utm_medium=readme&utm_campaign=foxplug_npm)
|
|
57
|
+
|
|
58
|
+
## What it sends
|
|
59
|
+
|
|
60
|
+
- **On a release tag** (found automatically when GitLab, Bitbucket, CircleCI or GitHub builds a tag, or passed with `--release`): the tag, its name, its notes and its link. The notes are the commit subjects since the previous tag, or a file you pass with `--notes`.
|
|
61
|
+
- **Otherwise**: the subject line of each commit in this push (GitLab gives the push's range), or of today's commits.
|
|
62
|
+
- FoxPlug writes a changelog entry and launch posts from it, as drafts. **Nothing is posted until you approve it.**
|
|
63
|
+
- The command prints in one sentence what happened, for example that a draft is waiting.
|
|
64
|
+
|
|
65
|
+
**Nothing else leaves the machine: no source code, no secrets, no environment.** `npx foxplug --dry-run` prints exactly what would be sent. The command is one file with no dependencies, [foxplug.js](https://github.com/OsakaSaul/foxplug-changelog-action/blob/main/cli/foxplug.js).
|
|
66
|
+
|
|
67
|
+
## Options
|
|
68
|
+
|
|
69
|
+
| Option | What it is |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `--release <tag>` | Send this tag as a release. |
|
|
72
|
+
| `--notes <file>` | Release notes from a file. |
|
|
73
|
+
| `--since <ref>` | Send the commits after this ref. |
|
|
74
|
+
| `--repo <owner/name>` | The repository name, if it cannot be read from git or CI. |
|
|
75
|
+
| `--project <name>` | Your FoxPlug project name, as a check that the token is the one you meant. |
|
|
76
|
+
| `--dry-run` | Print what would be sent; send nothing. |
|
|
77
|
+
|
|
78
|
+
A wrong or revoked token stops the step with a message. FoxPlug being unreachable never fails your build.
|
|
79
|
+
|
|
80
|
+
## Revoking
|
|
81
|
+
|
|
82
|
+
Revoke the token in your FoxPlug project at any time; the next run then stops with a message saying so.
|
|
83
|
+
|
|
84
|
+
## Licence
|
|
85
|
+
|
|
86
|
+
MIT
|
package/foxplug.js
CHANGED
|
@@ -1,178 +1,178 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// foxplug: the FoxPlug changelog Action for every other place your code runs (GitLab CI, Bitbucket
|
|
3
|
-
// Pipelines, CircleCI, Jenkins, a terminal). One file, no dependencies, Node 18+ built-in fetch.
|
|
4
|
-
//
|
|
5
|
-
// On a release tag: sends the tag, its name, its notes (the commit subjects since the previous tag
|
|
6
|
-
// unless you pass --notes) and its link.
|
|
7
|
-
// Otherwise: sends the subject line of each commit in this push (or of today's commits).
|
|
8
|
-
// Nothing else leaves the machine: no source code, no secrets, no environment.
|
|
9
|
-
//
|
|
10
|
-
// FoxPlug turns it into a changelog entry and launch posts in your project, as drafts waiting for
|
|
11
|
-
// your approval. Nothing is posted anywhere by this command.
|
|
12
|
-
|
|
13
|
-
"use strict";
|
|
14
|
-
const { execFileSync } = require("child_process");
|
|
15
|
-
const fs = require("fs");
|
|
16
|
-
|
|
17
|
-
const VERSION = "1.0.0";
|
|
18
|
-
const ENDPOINT = process.env.FOXPLUG_ENDPOINT || "https://fybedvapqhhgctkeqvbs.supabase.co/functions/v1/lcnc-action-ingest";
|
|
19
|
-
const USER_AGENT = `foxplug-npm/${VERSION} (+https://www.npmjs.com/package/foxplug)`;
|
|
20
|
-
const TOKEN_PAGE = "https://foxplug.com/app/?connect=github-action&utm_source=npm&utm_medium=cli&utm_campaign=foxplug_npm";
|
|
21
|
-
const env = process.env;
|
|
22
|
-
|
|
23
|
-
const HELP = `foxplug ${VERSION}: every release or push becomes a FoxPlug changelog entry and launch posts, as drafts.
|
|
24
|
-
|
|
25
|
-
Usage: npx foxplug [options]
|
|
26
|
-
|
|
27
|
-
FOXPLUG_TOKEN your project token (environment variable, required)
|
|
28
|
-
get one: ${TOKEN_PAGE}
|
|
29
|
-
|
|
30
|
-
--release <tag> send this tag as a release (found automatically in GitLab, Bitbucket,
|
|
31
|
-
CircleCI and GitHub when the build runs for a tag)
|
|
32
|
-
--notes <file> release notes from a file, instead of the commit subjects since the last tag
|
|
33
|
-
--since <ref> send the commits after this ref (default: the push's own range in CI,
|
|
34
|
-
otherwise today's commits)
|
|
35
|
-
--repo <owner/name> the repository name, if it cannot be read from git or CI
|
|
36
|
-
--project <name> your FoxPlug project name, as a check that the token is the one you meant
|
|
37
|
-
--dry-run print what would be sent, send nothing
|
|
38
|
-
--version, --help
|
|
39
|
-
|
|
40
|
-
Only the tag, its name, notes and link, or commit subject lines are sent.`;
|
|
41
|
-
|
|
42
|
-
function parseArgs(argv) {
|
|
43
|
-
const o = {};
|
|
44
|
-
for (let i = 0; i < argv.length; i++) {
|
|
45
|
-
const a = argv[i];
|
|
46
|
-
const take = () => { const v = argv[++i]; if (v == null || v.startsWith("--")) fail(`${a} needs a value.`); return v; };
|
|
47
|
-
if (a === "--help" || a === "-h") o.help = true;
|
|
48
|
-
else if (a === "--version" || a === "-v") o.version = true;
|
|
49
|
-
else if (a === "--dry-run") o.dryRun = true;
|
|
50
|
-
else if (a === "--release") o.release = take();
|
|
51
|
-
else if (a === "--notes") o.notes = take();
|
|
52
|
-
else if (a === "--since") o.since = take();
|
|
53
|
-
else if (a === "--repo") o.repo = take();
|
|
54
|
-
else if (a === "--project") o.project = take();
|
|
55
|
-
else fail(`Unknown option ${a}. Run npx foxplug --help.`);
|
|
56
|
-
}
|
|
57
|
-
return o;
|
|
58
|
-
}
|
|
59
|
-
function fail(msg) { console.error(`foxplug: ${msg}`); process.exit(1); }
|
|
60
|
-
function git(args) {
|
|
61
|
-
try { return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
|
62
|
-
catch (_e) { return null; }
|
|
63
|
-
}
|
|
64
|
-
const subjectOf = (m) => String(m || "").split("\n")[0].trim().slice(0, 300);
|
|
65
|
-
const zeroSha = (s) => !s || /^0+$/.test(s);
|
|
66
|
-
|
|
67
|
-
// owner/name, from the CI's own variable first, then the origin remote. GitLab subgroups keep the
|
|
68
|
-
// last two parts, the same way FoxPlug names the repository.
|
|
69
|
-
function repoName(o) {
|
|
70
|
-
const lastTwo = (p) => { const parts = String(p || "").replace(/\.git$/, "").split(/[/:]/).filter(Boolean); return parts.length >= 2 ? parts.slice(-2).join("/") : ""; };
|
|
71
|
-
const cands = [
|
|
72
|
-
o.repo, env.FOXPLUG_REPO, env.GITHUB_REPOSITORY, env.CI_PROJECT_PATH, env.BITBUCKET_REPO_FULL_NAME,
|
|
73
|
-
env.CIRCLE_PROJECT_USERNAME && env.CIRCLE_PROJECT_REPONAME ? `${env.CIRCLE_PROJECT_USERNAME}/${env.CIRCLE_PROJECT_REPONAME}` : "",
|
|
74
|
-
git(["remote", "get-url", "origin"]),
|
|
75
|
-
];
|
|
76
|
-
for (const c of cands) { const r = lastTwo(c); if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(r)) return r; }
|
|
77
|
-
return "";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function releaseTag(o) {
|
|
81
|
-
if (o.release) return o.release;
|
|
82
|
-
if (env.CI_COMMIT_TAG) return env.CI_COMMIT_TAG;
|
|
83
|
-
if (env.BITBUCKET_TAG) return env.BITBUCKET_TAG;
|
|
84
|
-
if (env.CIRCLE_TAG) return env.CIRCLE_TAG;
|
|
85
|
-
if (env.GITHUB_REF_TYPE === "tag" && env.GITHUB_REF_NAME) return env.GITHUB_REF_NAME;
|
|
86
|
-
return "";
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// The tag's own page, where the host has one we can name without an API call.
|
|
90
|
-
function tagUrl(tag) {
|
|
91
|
-
const enc = encodeURIComponent(tag);
|
|
92
|
-
if (env.CI_PROJECT_URL) return `${env.CI_PROJECT_URL}/-/tags/${enc}`;
|
|
93
|
-
if (env.BITBUCKET_GIT_HTTP_ORIGIN) return `${env.BITBUCKET_GIT_HTTP_ORIGIN}/src/${enc}`;
|
|
94
|
-
if (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY) return `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/releases/tag/${enc}`;
|
|
95
|
-
const origin = git(["remote", "get-url", "origin"]) || "";
|
|
96
|
-
const m = origin.match(/^(?:https:\/\/|git@)(github\.com|gitlab\.com|bitbucket\.org)[/:](.+?)(?:\.git)?$/);
|
|
97
|
-
if (!m) return "";
|
|
98
|
-
if (m[1] === "github.com") return `https://github.com/${m[2]}/releases/tag/${enc}`;
|
|
99
|
-
if (m[1] === "gitlab.com") return `https://gitlab.com/${m[2]}/-/tags/${enc}`;
|
|
100
|
-
return `https://bitbucket.org/${m[2]}/src/${enc}`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function logSubjects(range, sinceIso) {
|
|
104
|
-
const args = ["log", "--format=%s", "-n", "200"];
|
|
105
|
-
if (range) args.push(range); else args.push(`--since=${sinceIso}`, "HEAD");
|
|
106
|
-
const out = git(args);
|
|
107
|
-
if (out == null) return null;
|
|
108
|
-
return out.split("\n").map(subjectOf).filter(Boolean).reverse();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function buildRelease(tag, o) {
|
|
112
|
-
// Only an annotated tag has a name of its own; a lightweight tag would report its commit's subject.
|
|
113
|
-
const annotated = git(["tag", "-l", "--format=%(objecttype)", tag]) === "tag";
|
|
114
|
-
const name = (annotated && subjectOf(git(["tag", "-l", "--format=%(contents:subject)", tag]))) || tag;
|
|
115
|
-
let notes = "";
|
|
116
|
-
if (o.notes) {
|
|
117
|
-
try { notes = fs.readFileSync(o.notes, "utf8").trim(); } catch (e) { fail(`Could not read ${o.notes}: ${e.message}`); }
|
|
118
|
-
} else {
|
|
119
|
-
const prev = git(["describe", "--tags", "--abbrev=0", `${tag}^`]);
|
|
120
|
-
const subs = logSubjects(prev ? `${prev}..${tag}` : tag, null) || [];
|
|
121
|
-
notes = subs.slice(-60).map((s) => `- ${s}`).join("\n");
|
|
122
|
-
}
|
|
123
|
-
return { event: "release", release: { name: name === tag ? tag : `${tag}: ${name}`, tag, notes: notes.slice(0, 3800), url: tagUrl(tag) } };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function buildPush(o) {
|
|
127
|
-
if (git(["rev-parse", "--is-inside-work-tree"]) !== "true") fail("This is not a git repository. Run it from your repository's folder.");
|
|
128
|
-
let range = "";
|
|
129
|
-
const before = o.since || env.CI_COMMIT_BEFORE_SHA || env.FOXPLUG_SINCE || "";
|
|
130
|
-
if (!zeroSha(before)) range = `${before}..HEAD`;
|
|
131
|
-
const today = new Date().toISOString().slice(0, 10) + "T00:00:00Z";
|
|
132
|
-
let subjects = logSubjects(range, today);
|
|
133
|
-
if (subjects == null && range) {
|
|
134
|
-
console.log(`foxplug: ${before} is not in this checkout (a shallow clone?), so today's commits are sent instead.`);
|
|
135
|
-
subjects = logSubjects("", today);
|
|
136
|
-
}
|
|
137
|
-
if (subjects == null) fail("git log failed. Is git installed, and does the checkout have history?");
|
|
138
|
-
const branch = env.CI_COMMIT_BRANCH || env.BITBUCKET_BRANCH || env.CIRCLE_BRANCH || env.GITHUB_REF_NAME || git(["rev-parse", "--abbrev-ref", "HEAD"]) || "";
|
|
139
|
-
return { event: "push", branch, commits: [...new Set(subjects)].map((subject) => ({ subject })) };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function main() {
|
|
143
|
-
const o = parseArgs(process.argv.slice(2));
|
|
144
|
-
if (o.help) { console.log(HELP); return; }
|
|
145
|
-
if (o.version) { console.log(VERSION); return; }
|
|
146
|
-
|
|
147
|
-
const repo = repoName(o);
|
|
148
|
-
if (!repo) fail("Could not tell the repository's name. Pass --repo owner/name.");
|
|
149
|
-
const tag = releaseTag(o);
|
|
150
|
-
const payload = { ...(tag ? buildRelease(tag, o) : buildPush(o)), repo, client: "npm" };
|
|
151
|
-
if (o.project) payload.project = o.project;
|
|
152
|
-
if (payload.event === "push" && !payload.commits.length) {
|
|
153
|
-
console.log("foxplug: no new commits, so nothing is sent."); return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (o.dryRun) { console.log(JSON.stringify(payload, null, 2)); console.log("foxplug: dry run, nothing was sent."); return; }
|
|
157
|
-
const token = String(env.FOXPLUG_TOKEN || "").trim();
|
|
158
|
-
if (!token) fail(`No FOXPLUG_TOKEN. Get your project token at ${TOKEN_PAGE} and set it as a secret variable named FOXPLUG_TOKEN.`);
|
|
159
|
-
|
|
160
|
-
let res, body = {};
|
|
161
|
-
try {
|
|
162
|
-
res = await fetch(ENDPOINT, {
|
|
163
|
-
method: "POST",
|
|
164
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, "User-Agent": USER_AGENT },
|
|
165
|
-
body: JSON.stringify(payload),
|
|
166
|
-
signal: AbortSignal.timeout(120000),
|
|
167
|
-
});
|
|
168
|
-
body = await res.json().catch(() => ({}));
|
|
169
|
-
} catch (e) {
|
|
170
|
-
console.log(`foxplug: FoxPlug could not be reached (${e.message}). Your build is not affected; the next run sends again.`);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
console.log(`foxplug: ${String(body.message || `FoxPlug answered ${res.status}.`)}`);
|
|
174
|
-
// A wrong or revoked token stops the step so it gets noticed; FoxPlug being down never fails a build.
|
|
175
|
-
if (res.status === 400 || res.status === 401 || res.status === 404) process.exitCode = 1;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// foxplug: the FoxPlug changelog Action for every other place your code runs (GitLab CI, Bitbucket
|
|
3
|
+
// Pipelines, CircleCI, Jenkins, a terminal). One file, no dependencies, Node 18+ built-in fetch.
|
|
4
|
+
//
|
|
5
|
+
// On a release tag: sends the tag, its name, its notes (the commit subjects since the previous tag
|
|
6
|
+
// unless you pass --notes) and its link.
|
|
7
|
+
// Otherwise: sends the subject line of each commit in this push (or of today's commits).
|
|
8
|
+
// Nothing else leaves the machine: no source code, no secrets, no environment.
|
|
9
|
+
//
|
|
10
|
+
// FoxPlug turns it into a changelog entry and launch posts in your project, as drafts waiting for
|
|
11
|
+
// your approval. Nothing is posted anywhere by this command.
|
|
12
|
+
|
|
13
|
+
"use strict";
|
|
14
|
+
const { execFileSync } = require("child_process");
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
|
|
17
|
+
const VERSION = "1.0.0";
|
|
18
|
+
const ENDPOINT = process.env.FOXPLUG_ENDPOINT || "https://fybedvapqhhgctkeqvbs.supabase.co/functions/v1/lcnc-action-ingest";
|
|
19
|
+
const USER_AGENT = `foxplug-npm/${VERSION} (+https://www.npmjs.com/package/foxplug)`;
|
|
20
|
+
const TOKEN_PAGE = "https://foxplug.com/app/?connect=github-action&utm_source=npm&utm_medium=cli&utm_campaign=foxplug_npm";
|
|
21
|
+
const env = process.env;
|
|
22
|
+
|
|
23
|
+
const HELP = `foxplug ${VERSION}: every release or push becomes a FoxPlug changelog entry and launch posts, as drafts.
|
|
24
|
+
|
|
25
|
+
Usage: npx foxplug [options]
|
|
26
|
+
|
|
27
|
+
FOXPLUG_TOKEN your project token (environment variable, required)
|
|
28
|
+
get one: ${TOKEN_PAGE}
|
|
29
|
+
|
|
30
|
+
--release <tag> send this tag as a release (found automatically in GitLab, Bitbucket,
|
|
31
|
+
CircleCI and GitHub when the build runs for a tag)
|
|
32
|
+
--notes <file> release notes from a file, instead of the commit subjects since the last tag
|
|
33
|
+
--since <ref> send the commits after this ref (default: the push's own range in CI,
|
|
34
|
+
otherwise today's commits)
|
|
35
|
+
--repo <owner/name> the repository name, if it cannot be read from git or CI
|
|
36
|
+
--project <name> your FoxPlug project name, as a check that the token is the one you meant
|
|
37
|
+
--dry-run print what would be sent, send nothing
|
|
38
|
+
--version, --help
|
|
39
|
+
|
|
40
|
+
Only the tag, its name, notes and link, or commit subject lines are sent.`;
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const o = {};
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const a = argv[i];
|
|
46
|
+
const take = () => { const v = argv[++i]; if (v == null || v.startsWith("--")) fail(`${a} needs a value.`); return v; };
|
|
47
|
+
if (a === "--help" || a === "-h") o.help = true;
|
|
48
|
+
else if (a === "--version" || a === "-v") o.version = true;
|
|
49
|
+
else if (a === "--dry-run") o.dryRun = true;
|
|
50
|
+
else if (a === "--release") o.release = take();
|
|
51
|
+
else if (a === "--notes") o.notes = take();
|
|
52
|
+
else if (a === "--since") o.since = take();
|
|
53
|
+
else if (a === "--repo") o.repo = take();
|
|
54
|
+
else if (a === "--project") o.project = take();
|
|
55
|
+
else fail(`Unknown option ${a}. Run npx foxplug --help.`);
|
|
56
|
+
}
|
|
57
|
+
return o;
|
|
58
|
+
}
|
|
59
|
+
function fail(msg) { console.error(`foxplug: ${msg}`); process.exit(1); }
|
|
60
|
+
function git(args) {
|
|
61
|
+
try { return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
|
62
|
+
catch (_e) { return null; }
|
|
63
|
+
}
|
|
64
|
+
const subjectOf = (m) => String(m || "").split("\n")[0].trim().slice(0, 300);
|
|
65
|
+
const zeroSha = (s) => !s || /^0+$/.test(s);
|
|
66
|
+
|
|
67
|
+
// owner/name, from the CI's own variable first, then the origin remote. GitLab subgroups keep the
|
|
68
|
+
// last two parts, the same way FoxPlug names the repository.
|
|
69
|
+
function repoName(o) {
|
|
70
|
+
const lastTwo = (p) => { const parts = String(p || "").replace(/\.git$/, "").split(/[/:]/).filter(Boolean); return parts.length >= 2 ? parts.slice(-2).join("/") : ""; };
|
|
71
|
+
const cands = [
|
|
72
|
+
o.repo, env.FOXPLUG_REPO, env.GITHUB_REPOSITORY, env.CI_PROJECT_PATH, env.BITBUCKET_REPO_FULL_NAME,
|
|
73
|
+
env.CIRCLE_PROJECT_USERNAME && env.CIRCLE_PROJECT_REPONAME ? `${env.CIRCLE_PROJECT_USERNAME}/${env.CIRCLE_PROJECT_REPONAME}` : "",
|
|
74
|
+
git(["remote", "get-url", "origin"]),
|
|
75
|
+
];
|
|
76
|
+
for (const c of cands) { const r = lastTwo(c); if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(r)) return r; }
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function releaseTag(o) {
|
|
81
|
+
if (o.release) return o.release;
|
|
82
|
+
if (env.CI_COMMIT_TAG) return env.CI_COMMIT_TAG;
|
|
83
|
+
if (env.BITBUCKET_TAG) return env.BITBUCKET_TAG;
|
|
84
|
+
if (env.CIRCLE_TAG) return env.CIRCLE_TAG;
|
|
85
|
+
if (env.GITHUB_REF_TYPE === "tag" && env.GITHUB_REF_NAME) return env.GITHUB_REF_NAME;
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The tag's own page, where the host has one we can name without an API call.
|
|
90
|
+
function tagUrl(tag) {
|
|
91
|
+
const enc = encodeURIComponent(tag);
|
|
92
|
+
if (env.CI_PROJECT_URL) return `${env.CI_PROJECT_URL}/-/tags/${enc}`;
|
|
93
|
+
if (env.BITBUCKET_GIT_HTTP_ORIGIN) return `${env.BITBUCKET_GIT_HTTP_ORIGIN}/src/${enc}`;
|
|
94
|
+
if (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY) return `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/releases/tag/${enc}`;
|
|
95
|
+
const origin = git(["remote", "get-url", "origin"]) || "";
|
|
96
|
+
const m = origin.match(/^(?:https:\/\/|git@)(github\.com|gitlab\.com|bitbucket\.org)[/:](.+?)(?:\.git)?$/);
|
|
97
|
+
if (!m) return "";
|
|
98
|
+
if (m[1] === "github.com") return `https://github.com/${m[2]}/releases/tag/${enc}`;
|
|
99
|
+
if (m[1] === "gitlab.com") return `https://gitlab.com/${m[2]}/-/tags/${enc}`;
|
|
100
|
+
return `https://bitbucket.org/${m[2]}/src/${enc}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function logSubjects(range, sinceIso) {
|
|
104
|
+
const args = ["log", "--format=%s", "-n", "200"];
|
|
105
|
+
if (range) args.push(range); else args.push(`--since=${sinceIso}`, "HEAD");
|
|
106
|
+
const out = git(args);
|
|
107
|
+
if (out == null) return null;
|
|
108
|
+
return out.split("\n").map(subjectOf).filter(Boolean).reverse();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildRelease(tag, o) {
|
|
112
|
+
// Only an annotated tag has a name of its own; a lightweight tag would report its commit's subject.
|
|
113
|
+
const annotated = git(["tag", "-l", "--format=%(objecttype)", tag]) === "tag";
|
|
114
|
+
const name = (annotated && subjectOf(git(["tag", "-l", "--format=%(contents:subject)", tag]))) || tag;
|
|
115
|
+
let notes = "";
|
|
116
|
+
if (o.notes) {
|
|
117
|
+
try { notes = fs.readFileSync(o.notes, "utf8").trim(); } catch (e) { fail(`Could not read ${o.notes}: ${e.message}`); }
|
|
118
|
+
} else {
|
|
119
|
+
const prev = git(["describe", "--tags", "--abbrev=0", `${tag}^`]);
|
|
120
|
+
const subs = logSubjects(prev ? `${prev}..${tag}` : tag, null) || [];
|
|
121
|
+
notes = subs.slice(-60).map((s) => `- ${s}`).join("\n");
|
|
122
|
+
}
|
|
123
|
+
return { event: "release", release: { name: name === tag ? tag : `${tag}: ${name}`, tag, notes: notes.slice(0, 3800), url: tagUrl(tag) } };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildPush(o) {
|
|
127
|
+
if (git(["rev-parse", "--is-inside-work-tree"]) !== "true") fail("This is not a git repository. Run it from your repository's folder.");
|
|
128
|
+
let range = "";
|
|
129
|
+
const before = o.since || env.CI_COMMIT_BEFORE_SHA || env.FOXPLUG_SINCE || "";
|
|
130
|
+
if (!zeroSha(before)) range = `${before}..HEAD`;
|
|
131
|
+
const today = new Date().toISOString().slice(0, 10) + "T00:00:00Z";
|
|
132
|
+
let subjects = logSubjects(range, today);
|
|
133
|
+
if (subjects == null && range) {
|
|
134
|
+
console.log(`foxplug: ${before} is not in this checkout (a shallow clone?), so today's commits are sent instead.`);
|
|
135
|
+
subjects = logSubjects("", today);
|
|
136
|
+
}
|
|
137
|
+
if (subjects == null) fail("git log failed. Is git installed, and does the checkout have history?");
|
|
138
|
+
const branch = env.CI_COMMIT_BRANCH || env.BITBUCKET_BRANCH || env.CIRCLE_BRANCH || env.GITHUB_REF_NAME || git(["rev-parse", "--abbrev-ref", "HEAD"]) || "";
|
|
139
|
+
return { event: "push", branch, commits: [...new Set(subjects)].map((subject) => ({ subject })) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function main() {
|
|
143
|
+
const o = parseArgs(process.argv.slice(2));
|
|
144
|
+
if (o.help) { console.log(HELP); return; }
|
|
145
|
+
if (o.version) { console.log(VERSION); return; }
|
|
146
|
+
|
|
147
|
+
const repo = repoName(o);
|
|
148
|
+
if (!repo) fail("Could not tell the repository's name. Pass --repo owner/name.");
|
|
149
|
+
const tag = releaseTag(o);
|
|
150
|
+
const payload = { ...(tag ? buildRelease(tag, o) : buildPush(o)), repo, client: "npm" };
|
|
151
|
+
if (o.project) payload.project = o.project;
|
|
152
|
+
if (payload.event === "push" && !payload.commits.length) {
|
|
153
|
+
console.log("foxplug: no new commits, so nothing is sent."); return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (o.dryRun) { console.log(JSON.stringify(payload, null, 2)); console.log("foxplug: dry run, nothing was sent."); return; }
|
|
157
|
+
const token = String(env.FOXPLUG_TOKEN || "").trim();
|
|
158
|
+
if (!token) fail(`No FOXPLUG_TOKEN. Get your project token at ${TOKEN_PAGE} and set it as a secret variable named FOXPLUG_TOKEN.`);
|
|
159
|
+
|
|
160
|
+
let res, body = {};
|
|
161
|
+
try {
|
|
162
|
+
res = await fetch(ENDPOINT, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, "User-Agent": USER_AGENT },
|
|
165
|
+
body: JSON.stringify(payload),
|
|
166
|
+
signal: AbortSignal.timeout(120000),
|
|
167
|
+
});
|
|
168
|
+
body = await res.json().catch(() => ({}));
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.log(`foxplug: FoxPlug could not be reached (${e.message}). Your build is not affected; the next run sends again.`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
console.log(`foxplug: ${String(body.message || `FoxPlug answered ${res.status}.`)}`);
|
|
174
|
+
// A wrong or revoked token stops the step so it gets noticed; FoxPlug being down never fails a build.
|
|
175
|
+
if (res.status === 400 || res.status === 401 || res.status === 404) process.exitCode = 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "foxplug",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Every release or push becomes a changelog entry and ready-to-post launch updates in FoxPlug, waiting for your approval. Works in GitLab CI, Bitbucket Pipelines, CircleCI or a terminal.",
|
|
5
|
-
"bin": { "foxplug": "foxplug.js" },
|
|
6
|
-
"files": ["foxplug.js", "README.md", "LICENSE"],
|
|
7
|
-
"engines": { "node": ">=18" },
|
|
8
|
-
"keywords": ["changelog", "release-notes", "release", "launch", "build-in-public", "gitlab-ci", "bitbucket-pipelines", "circleci", "ci"],
|
|
9
|
-
"homepage": "https://foxplug.com/?utm_source=npm&utm_medium=package&utm_campaign=foxplug_npm",
|
|
10
|
-
"repository": { "type": "git", "url": "git+https://github.com/OsakaSaul/foxplug-changelog-action.git", "directory": "cli" },
|
|
11
|
-
"bugs": { "url": "https://github.com/OsakaSaul/foxplug-changelog-action/issues" },
|
|
12
|
-
"license": "MIT"
|
|
13
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "foxplug",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Every release or push becomes a changelog entry and ready-to-post launch updates in FoxPlug, waiting for your approval. Works in GitLab CI, Bitbucket Pipelines, CircleCI or a terminal.",
|
|
5
|
+
"bin": { "foxplug": "foxplug.js" },
|
|
6
|
+
"files": ["foxplug.js", "README.md", "LICENSE"],
|
|
7
|
+
"engines": { "node": ">=18" },
|
|
8
|
+
"keywords": ["changelog", "release-notes", "release", "launch", "build-in-public", "gitlab-ci", "bitbucket-pipelines", "circleci", "ci"],
|
|
9
|
+
"homepage": "https://foxplug.com/?utm_source=npm&utm_medium=package&utm_campaign=foxplug_npm",
|
|
10
|
+
"repository": { "type": "git", "url": "git+https://github.com/OsakaSaul/foxplug-changelog-action.git", "directory": "cli" },
|
|
11
|
+
"bugs": { "url": "https://github.com/OsakaSaul/foxplug-changelog-action/issues" },
|
|
12
|
+
"license": "MIT"
|
|
13
|
+
}
|