├── example
├── images
│ └── pr-comment.png
└── test-workflow.yml
├── action
├── context
│ ├── logos
│ │ ├── rollbar.png
│ │ ├── lightstep.png
│ │ └── pagerduty.png
│ ├── pagerduty.js
│ ├── rollbar.js
│ └── lightstep.js
├── config
│ └── index.js
├── utils.js
└── predeploy.js
├── .gitignore
├── .github
└── workflows
│ └── ci.yml
├── package.json
├── .eslintrc.json
├── index.js
├── action.yml
├── pr.tmpl.md
├── dist
├── pr.tmpl.md
├── dot2js.g
└── sourcemap-register.js
├── README.md
└── LICENSE
/example/images/pr-comment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightstep/lightstep-action-predeploy/HEAD/example/images/pr-comment.png
--------------------------------------------------------------------------------
/action/context/logos/rollbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightstep/lightstep-action-predeploy/HEAD/action/context/logos/rollbar.png
--------------------------------------------------------------------------------
/action/context/logos/lightstep.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightstep/lightstep-action-predeploy/HEAD/action/context/logos/lightstep.png
--------------------------------------------------------------------------------
/action/context/logos/pagerduty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightstep/lightstep-action-predeploy/HEAD/action/context/logos/pagerduty.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | .lightstep.yml
4 |
5 | # Optional npm cache directory
6 | .npm
7 |
8 | # Optional eslint cache
9 | .eslintcache
10 |
11 | # Editors
12 | .vscode
13 |
14 | # Logs
15 | logs
16 | *.log
17 | npm-debug.log*
18 | yarn-debug.log*
19 | yarn-error.log*
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: 'Continuous Integration'
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | test:
8 | name: Test
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v2
14 |
15 | - name: Setup Node.js 12.x
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: '12.x'
19 |
20 | - name: Install
21 | run: npm clean-install
22 |
23 | - name: Verify
24 | run: |
25 | npm run build
26 | # Fail if "npm run build" generated new changes in dist
27 | git update-index --refresh dist/* && git diff-index --quiet HEAD dist
28 |
--------------------------------------------------------------------------------
/action/config/index.js:
--------------------------------------------------------------------------------
1 | const yaml = require('js-yaml')
2 | const path = require('path')
3 | const fs = require('fs')
4 |
5 | const LIGHTSTEP_CONFIG_FILE = process.env.LIGHTSTEP_CONFIG_FILE || '.lightstep.yml'
6 |
7 | function configExists() {
8 | return fs.existsSync(path.join(process.env.GITHUB_WORKSPACE, LIGHTSTEP_CONFIG_FILE))
9 | }
10 |
11 | function loadConfig() {
12 | try {
13 | let fileContents = fs.readFileSync(path.join(process.env.GITHUB_WORKSPACE, LIGHTSTEP_CONFIG_FILE), 'utf8')
14 | const yamlConfig = yaml.safeLoadAll(fileContents)
15 | return yamlConfig[0]
16 | } catch (e) {
17 | return { integrations : {} }
18 | }
19 | }
20 |
21 | exports.configExists = configExists
22 | exports.loadConfig = loadConfig
--------------------------------------------------------------------------------
/example/test-workflow.yml:
--------------------------------------------------------------------------------
1 | name: Lightstep Pre-Deploy Check
2 | on:
3 | pull_request_review:
4 | types: [submitted]
5 |
6 | jobs:
7 | deploy_check_job:
8 | runs-on: ubuntu-latest
9 | name: Verify Pre-Deploy Status
10 |
11 | steps:
12 | # Using checkout is required if reading from a `.lightstep.yml` file in the repo
13 | - name: Checkout
14 | uses: actions/checkout@v2
15 |
16 | - name: Lightstep Pre-Deploy Check
17 | id: lightstep-predeploy
18 | uses: lightstep/lightstep-action-predeploy@master
19 | env:
20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21 | with:
22 | lightstep_api_key: ${{ secrets.LIGHTSTEP_API_TOKEN }}
23 | pagerduty_api_token: ${{ secrets.PAGERDUTY_API_TOKEN }}
24 | rollbar_api_token: ${{ secrets.ROLLBAR_API_TOKEN }}
25 |
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lightstep-action-predeploy",
3 | "version": "0.2.6",
4 | "description": "GitHub Action to see service health, reported errors, and on-call information in pull requests.",
5 | "main": "index.js",
6 | "scripts": {
7 | "lint": "eslint .",
8 | "build": "ncc build index.js -o dist --source-map",
9 | "test": "jest",
10 | "all": "npm run lint && npm run prepare && npm run test"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "@actions/core": "^1.2.6",
17 | "@actions/github": "^4.0.0",
18 | "js-yaml": "^3.14.0",
19 | "lightstep-js-sdk": "git://github.com/lightstep/lightstep-js-sdk.git",
20 | "lodash.template": "^4.5.0",
21 | "node-fetch": "^2.6.1"
22 | },
23 | "devDependencies": {
24 | "@zeit/ncc": "^0.22.3",
25 | "eslint": "^7.12.1",
26 | "jest": "^26.6.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/action/utils.js:
--------------------------------------------------------------------------------
1 | const core = require('@actions/core')
2 | const assert = require('assert')
3 |
4 | /**
5 | * Resolves input as an enviornment variable or action input
6 | * @param {*} name input name
7 | */
8 | const resolveActionInput = (name, config = {}) => {
9 | if (typeof name !== 'string') {
10 | return null
11 | }
12 | const configName = name.replace('lightstep_', '')
13 | return process.env[name.toUpperCase()] || core.getInput(name) || config[configName]
14 | }
15 |
16 | /**
17 | * Fails action if input does not exist
18 | * @param {*} name input name
19 | */
20 | const assertActionInput = (name, config) => {
21 | if (!resolveActionInput(name, config)) {
22 | const msg = `Input ${name} must be set as an env var, passed as an action input, or specified in .lightstep.yml`
23 | core.setFailed(msg)
24 | assert.fail(msg)
25 | }
26 | }
27 |
28 | module.exports = { assertActionInput, resolveActionInput }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "commonjs": true,
4 | "es6": true,
5 | "jest": true,
6 | "node": true
7 | },
8 | "ignorePatterns": ["dist/*.js"],
9 | "extends": "eslint:recommended",
10 | "globals": {
11 | "Atomics": "readonly",
12 | "SharedArrayBuffer": "readonly"
13 | },
14 | "parserOptions": {
15 | "ecmaVersion": 2018
16 | },
17 | "rules": {
18 | "array-bracket-spacing": [0],
19 | "consistent-return": [0],
20 | "func-names": [0],
21 | "guard-for-in": [0],
22 | "indent": [2, 4],
23 | "semi": ["error", "never"],
24 | "key-spacing": [2, { "align": "colon", "beforeColon": true, "afterColon": true }],
25 | "max-len": [2, 120, 4],
26 | "new-cap": [0],
27 | "no-trailing-spaces": "error",
28 | "no-continue": [0],
29 | "no-param-reassign": [0],
30 | "no-multi-spaces": [2, { "exceptions": {
31 | "AssignmentExpression": true,
32 | "VariableDeclarator": true
33 | }} ],
34 | "no-underscore-dangle": [0],
35 | "no-unused-vars": [2, { "vars": "all", "args" : "none" }],
36 | "no-use-before-define": [2, { "functions": false }],
37 | "no-console": [1],
38 | "object-shorthand" : [0],
39 | "prefer-const": [0],
40 | "prefer-rest-params" : [0],
41 | "spaced-comment": [0]
42 | }
43 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const core = require('@actions/core')
2 | const config = require('./action/config')
3 | const { assertActionInput, resolveActionInput } = require('./action/utils')
4 | const { predeploy } = require('./action/predeploy')
5 |
6 | async function run() {
7 | try {
8 | const yamlFile = config.loadConfig()
9 |
10 | assertActionInput('lightstep_api_key')
11 | assertActionInput('lightstep_organization', yamlFile)
12 | assertActionInput('lightstep_project', yamlFile)
13 | const lightstepOrg = resolveActionInput('lightstep_organization', yamlFile)
14 | const lightstepProj = resolveActionInput('lightstep_project', yamlFile)
15 | const lightstepToken = resolveActionInput('lightstep_api_key')
16 | core.info(`Using Lightstep organization: ${lightstepOrg}`)
17 | core.info(`Using Lightstep project: ${lightstepProj}`)
18 |
19 | const isRollup = resolveActionInput('rollup_conditions') === 'true'
20 | if (isRollup) {
21 | core.info('Rolling up conditions in table...')
22 | }
23 | await predeploy({ lightstepOrg, lightstepProj, lightstepToken, yamlFile, isRollup })
24 |
25 | core.setOutput('lightstep_organization', lightstepOrg)
26 | core.setOutput('lightstep_project', lightstepProj)
27 | } catch (error) {
28 | core.setFailed(error.message)
29 | }
30 | }
31 |
32 | run()
33 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Lightstep Pre-Deploy Check'
2 | author: 'Lightstep, Inc.'
3 | description: 'View service health, reported errors, and on-call information in pull requests.'
4 |
5 | branding:
6 | icon: 'alert-triangle'
7 | color: 'green'
8 |
9 | inputs:
10 | lightstep_organization:
11 | description: 'The organization associated with your Lightstep account (usually your company name)'
12 | required: false
13 |
14 | lightstep_project:
15 | description: 'The Lightstep project associated with this repository'
16 | required: false
17 |
18 | lightstep_api_key:
19 | description: 'The key to access the Lightstep Public API'
20 | required: false
21 |
22 | rollbar_api_token:
23 | description: 'The token to access the Rollbar API'
24 | required: false
25 |
26 | pagerduty_api_token:
27 | description: 'The token to access the PagerDuty API'
28 | required: false
29 |
30 | rollup_conditions:
31 | description: 'If set to true, collapse all conditions to a single table row'
32 | required: false
33 |
34 | disable_comment:
35 | description: 'If set to true, will not add a comment to pull-requests'
36 | required: false
37 |
38 | outputs:
39 | lightstep_predeploy_status:
40 | description: 'Status of pre-deploy checks after running (error, ok, or unknown)'
41 |
42 | lightstep_predeploy_md:
43 | description: 'Markdown summary of pre-deploy checks'
44 |
45 | runs:
46 | using: 'node12'
47 | main: 'dist/index.js'
48 |
--------------------------------------------------------------------------------
/pr.tmpl.md:
--------------------------------------------------------------------------------
1 | <% hasError = (status === 'warn' || status === 'error' || status === 'unknown') %>
2 | <% if (hasError) { %>### :warning: Deploy with caution :warning:
3 |
4 | > There are errors or warnings in production.<% } %>
5 | <% if (!hasError) { %>### :100: Pre-deploy Checks Passed :100: <% } %>
6 | ### System Health
7 | | Status | External Link | Summary |
8 | |:-:|--|--|<% if (isRollup) { %>
9 | | <%=trafficLightStatus(lightstep.status)%> |
[Monitoring Conditions](<%=lightstep.summaryLink%>) | _<%=lightstep.message%>_ |<% } else { %><% if (lightstep.context) { for (var i=0; i < lightstep.context.length; i++) { %>
10 | | <%=conditionStatus(lightstep.context[i])%> |
[<%= lightstep.context[i].name %>](<%=lightstep.context[i].streamLink%>) | value: `<%= lightstep.context[i].description %>` |<% } %><% } %>
11 | <% if (rollbar) { %>| <%=trafficLightStatus(rollbar.status)%> |
[New Items in Latest Version](<%=rollbar.summaryLink%>) | _<%=rollbar.message%>_ |<% } } %>
12 |
13 | <% if (pagerduty) { %>#### Incident Response
14 |
[<%=pagerduty.message%>](<%=pagerduty.summaryLink%>)<% } %>
15 |
16 | <% if (hasError && isRollup) { %>
17 |
18 | Lightstep has detected some conditions in an unknown or error state in the project.
19 |
20 |
21 | <% lightstep.details.forEach(function(c) { %><%=c.message%>
22 | <% }) %>
23 | <% } %>
--------------------------------------------------------------------------------
/dist/pr.tmpl.md:
--------------------------------------------------------------------------------
1 | <% hasError = (status === 'warn' || status === 'error' || status === 'unknown') %>
2 | <% if (hasError) { %>### :warning: Deploy with caution :warning:
3 |
4 | > There are errors or warnings in production.<% } %>
5 | <% if (!hasError) { %>### :100: Pre-deploy Checks Passed :100: <% } %>
6 | ### System Health
7 | | Status | External Link | Summary |
8 | |:-:|--|--|<% if (isRollup) { %>
9 | | <%=trafficLightStatus(lightstep.status)%> |
[Monitoring Conditions](<%=lightstep.summaryLink%>) | _<%=lightstep.message%>_ |<% } else { %><% if (lightstep.context) { for (var i=0; i < lightstep.context.length; i++) { %>
10 | | <%=conditionStatus(lightstep.context[i])%> |
[<%= lightstep.context[i].name %>](<%=lightstep.context[i].streamLink%>) | value: `<%= lightstep.context[i].description %>` |<% } %><% } %>
11 | <% if (rollbar) { %>| <%=trafficLightStatus(rollbar.status)%> |
[New Items in Latest Version](<%=rollbar.summaryLink%>) | _<%=rollbar.message%>_ |<% } } %>
12 |
13 | <% if (pagerduty) { %>#### Incident Response
14 |
[<%=pagerduty.message%>](<%=pagerduty.summaryLink%>)<% } %>
15 |
16 | <% if (hasError && isRollup) { %>
17 |
18 | Lightstep has detected some conditions in an unknown or error state in the project.
19 |
20 |
21 | <% lightstep.details.forEach(function(c) { %><%=c.message%>
22 | <% }) %>
23 | <% } %>
--------------------------------------------------------------------------------
/action/context/pagerduty.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch')
2 |
3 | const PD_API = 'https://api.pagerduty.com'
4 |
5 | const getApiContext = async ({token, service}) => {
6 | const HEADERS = {
7 | "Authorization" : `Token token=${token}`,
8 | "Accept" : "application/json"
9 | }
10 | const serviceResponse = await fetch(`${PD_API}/services/${service}`, { headers : HEADERS })
11 | if (serviceResponse.status !== 200) {
12 | throw new Error(`PagerDuty API error fetching service '${service}' ${serviceResponse.status}`)
13 | }
14 |
15 | const serviceJson = await serviceResponse.json()
16 | const escalationPolicyId = serviceJson.service.escalation_policy.id
17 |
18 | const onCallResponse = await fetch(`${PD_API}/oncalls?include[]=&escalation_policy_ids[]=${escalationPolicyId}`,
19 | { headers : HEADERS })
20 | if (onCallResponse.status !== 200) {
21 | throw new Error(`PagerDuty API error fetching oncalls ${serviceResponse.status}`)
22 | }
23 |
24 | const oncallsJson = await onCallResponse.json()
25 |
26 | return {
27 | service : serviceJson.service,
28 | oncalls : oncallsJson.oncalls
29 | }
30 | }
31 |
32 | const ICON_IMG = "https://user-images.githubusercontent.com/27153/90803915-4fe88400-e2ce-11ea-803f-47b9c244799d.png"
33 |
34 | exports.getSummary = async ({token, yamlConfig}) => {
35 | const { service } = yamlConfig
36 |
37 | try {
38 | const context = await getApiContext({token, service})
39 | var onCallNames = context.oncalls.map(o => o.user.summary)
40 | var summaryLink = context.service.html_url
41 | var message = `On-call for *${context.service.name}*: ${onCallNames}`
42 | return {
43 | status : "unknown",
44 | message,
45 | summaryLink,
46 | logo : ICON_IMG
47 | }
48 | } catch (e) {
49 | return {
50 | status : "unknown",
51 | message : `PagerDuty API Error: ${e.message}`,
52 | summaryLink : "http://www.pagerduty.com",
53 | logo : ICON_IMG
54 | }
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/action/context/rollbar.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch')
2 |
3 | const ROLLBAR_API = 'https://api.rollbar.com'
4 |
5 |
6 | const getApiContext = async ({token, environment}) => {
7 | const HEADERS = { "X-Rollbar-Access-Token" : token }
8 | const deployResponse = await fetch(`${ROLLBAR_API}/api/1/deploys`, { headers : HEADERS })
9 | if (deployResponse.status !== 200) {
10 | throw new Error(`Rollbar API Error: ${deployResponse.status}`)
11 | }
12 |
13 | const deploys = await deployResponse.json()
14 |
15 | if (deploys.err === 1) {
16 | throw new Error(deploys.message)
17 | }
18 |
19 | if (deploys.err === 0 && deploys.result.deploys.length === 0 ){
20 | return null
21 | }
22 |
23 | const lastDeploy = deploys.result.deploys[0]
24 | const versionsResponse =
25 | await fetch(`${ROLLBAR_API}/api/1/versions/${lastDeploy.revision}?environment=${environment}`,
26 | { headers : HEADERS })
27 | const versions = await versionsResponse.json()
28 |
29 | if (versions.err === 1) {
30 | throw new Error(versions.message)
31 | }
32 | return versions.result
33 | }
34 |
35 | const ICON_IMG = "https://user-images.githubusercontent.com/27153/90803304-65a97980-e2cd-11ea-8267-a711fdcc6bc9.png"
36 |
37 | exports.getSummary = async ({token, yamlConfig}) => {
38 | const { environment, account, project } = yamlConfig
39 |
40 | try {
41 | const context = await getApiContext({token, environment})
42 | if (context === null) {
43 | return {
44 |
45 | }
46 | }
47 | var status = "unknown"
48 | var message = "Rollbar data unavailable"
49 | const details = [
50 | {
51 | message : `Version ${context.version} has ${context.item_stats.new.critical} critical errors.`
52 | }
53 | ]
54 |
55 | const errors = context.item_stats.new.error
56 | if (errors > 0) {
57 | status = "warn"
58 | message = "New errors have been detected since last deploy"
59 | }
60 |
61 | const critical = context.item_stats.new.critical
62 | if (critical > 0) {
63 | status = "error"
64 | message = "New critical errors have been detected since last deploy"
65 | }
66 |
67 | if (errors + critical === 0) {
68 | status = "ok"
69 | message = "No new errors detected since last deploy"
70 | }
71 |
72 | // eslint-disable-next-line max-len
73 | const summaryLink = `https://rollbar.com/${account}/${project}/versions/${environment}/${context.version}`
74 |
75 | return {
76 | status,
77 | message,
78 | summaryLink,
79 | details,
80 | context,
81 | logo : ICON_IMG
82 | }
83 | } catch (e) {
84 | return {
85 | "status" : "unknown",
86 | "message" : `Rollbar API Error: ${e.message}`,
87 | logo : ICON_IMG,
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/action/context/lightstep.js:
--------------------------------------------------------------------------------
1 | const lightstepSdk = require('lightstep-js-sdk')
2 |
3 | const LIGHTSTEP_WEB_HOST = 'app.lightstep.com'
4 |
5 | const getApiContext = async ({lightstepProj, lightstepOrg, lightstepToken, lightstepConditions = []}) => {
6 | const apiClient = await lightstepSdk.init(lightstepOrg, lightstepToken)
7 | // if no conditions are specified, use all conditions from project
8 | var conditionsResponse = []
9 | var lightstepConditionIds = []
10 | var conditionStreams = {}
11 | if (lightstepConditions.length === 0) {
12 | conditionsResponse = await apiClient.sdk.apis.Conditions.listConditionsID({
13 | organization : lightstepOrg,
14 | project : lightstepProj
15 | })
16 | conditionsResponse = conditionsResponse.obj.data
17 | lightstepConditionIds = conditionsResponse.map(c => c.id)
18 | } else {
19 | const lightstepConditionPromises = lightstepConditions.map(id => {
20 | return apiClient.sdk.apis.Conditions.getConditionID(
21 | {'condition-id' : id, organization : lightstepOrg, project : lightstepProj})
22 | })
23 | const lightstepConditionsResp = await Promise.all(lightstepConditionPromises)
24 | conditionsResponse = lightstepConditionsResp.map(r => r.obj.data)
25 | lightstepConditionIds = conditionsResponse.map(c => c.id)
26 | }
27 | conditionStreams = conditionsResponse.reduce((obj, c) => {
28 | const parts = c.relationships.stream.links.related.split('/')
29 | obj[c.id] = parts[parts.length-1]
30 | return obj
31 | }, {})
32 | const conditionStatusPromises = lightstepConditionIds.map(
33 | id => apiClient.sdk.apis.Conditions.getConditionStatusID({
34 | 'condition-id' : id,
35 | organization : lightstepOrg,
36 | project : lightstepProj
37 | })
38 | )
39 | const conditionStatusResponses = await Promise.all(conditionStatusPromises)
40 | const conditionStatuses = conditionStatusResponses.map(s => {
41 | const cleanId = s.body.data.id.replace('-status', '')
42 | const streamLink =
43 | `https://${LIGHTSTEP_WEB_HOST}/${lightstepProj}/stream/${conditionStreams[cleanId]}?selected_condition_id=${cleanId}`
44 | return {
45 | id : cleanId,
46 | stream : conditionStreams[cleanId],
47 | streamLink : streamLink,
48 | name : s.body.data.attributes.expression,
49 | description : s.body.data.attributes.description,
50 | state : s.body.data.attributes.state
51 | }
52 | })
53 | return conditionStatuses
54 | }
55 |
56 | const ICON_IMG = "https://user-images.githubusercontent.com/27153/90803298-6510e300-e2cd-11ea-91fa-5795a4481e20.png"
57 |
58 | exports.getSummary = async ({lightstepProj, lightstepOrg, lightstepToken, lightstepConditions}) => {
59 | try {
60 | const context = await getApiContext({lightstepProj, lightstepOrg, lightstepToken, lightstepConditions})
61 |
62 | // todo: handle no conditions
63 |
64 | var status = "unknown"
65 | var message = "Condition status is unknown"
66 | const details = context.map(c => {
67 | return { message : `${c.name}: ${c.state}` }
68 | })
69 | const summaryLink = `https://${LIGHTSTEP_WEB_HOST}/${lightstepProj}/monitoring/conditions`
70 | const noViolations = context.filter(c => c.state === 'false')
71 | const violated = context.filter(c => c.state === 'true')
72 |
73 | if (noViolations.length === context.length) {
74 | status = "ok"
75 | message = "No conditions have violations"
76 | } else if (violated.length > 0) {
77 | status = "error"
78 | message = "Condition(s) have violations"
79 | }
80 |
81 | return {
82 | status,
83 | message,
84 | summaryLink,
85 | details,
86 | context,
87 | logo : ICON_IMG
88 | }
89 | } catch (e) {
90 | return {
91 | status : "unknown",
92 | message : `Lightstep API Error: ${e.message}`,
93 | summaryLink : "https://lightstep.com",
94 | details : [],
95 | logo : ICON_IMG
96 | }
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/dist/dot2js.g:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2010 Gregoire Lejeune
2 | //
3 | // This program is free software; you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation; either version 2 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program; if not, write to the Free Software
15 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 | //
17 | // Usage :
18 | // gvpr -f dot2js.g [-a